@sdk-it/cli 0.22.1 → 0.24.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.
package/dist/index.js CHANGED
@@ -1,13 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // packages/cli/src/lib/cli.ts
4
- import { Command as Command3, program } from "commander";
4
+ import { Command as Command5, program } from "commander";
5
5
 
6
- // packages/cli/src/lib/langs/dart.ts
6
+ // packages/cli/src/lib/apiref.ts
7
7
  import { Command } from "commander";
8
- import { execFile, execSync } from "node:child_process";
9
- import { generate } from "@sdk-it/dart";
10
- import { augmentSpec, loadSpec } from "@sdk-it/spec";
8
+ import { execa } from "execa";
11
9
 
12
10
  // packages/cli/src/lib/options.ts
13
11
  import { Option } from "commander";
@@ -22,14 +20,71 @@ var outputOption = new Option(
22
20
  function shellEnv(name) {
23
21
  return process.platform === "win32" ? `%${name}%` : `$${name}`;
24
22
  }
23
+ function parseDotConfig(incoming) {
24
+ if (incoming === "false") {
25
+ return false;
26
+ }
27
+ if (incoming === "true") {
28
+ return true;
29
+ }
30
+ if (!incoming) {
31
+ return void 0;
32
+ }
33
+ const config = {};
34
+ const pairs = incoming.split(",");
35
+ for (const pair of pairs) {
36
+ if (pair.includes("=")) {
37
+ const [key, val] = pair.split("=", 2);
38
+ if (val === "true") {
39
+ config[key] = true;
40
+ continue;
41
+ }
42
+ if (val === "false") {
43
+ config[key] = false;
44
+ continue;
45
+ }
46
+ config[key] = val;
47
+ }
48
+ }
49
+ return config;
50
+ }
51
+ function parsePagination(config) {
52
+ if (config === true || config === void 0) {
53
+ return void 0;
54
+ }
55
+ if (config === false) {
56
+ return false;
57
+ }
58
+ return config;
59
+ }
60
+
61
+ // packages/cli/src/lib/apiref.ts
62
+ var apiref_default = new Command("apiref").description("Generate APIREF").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(true)).action(async (options) => {
63
+ await execa("nx", ["run", "apiref:build"], {
64
+ stdio: "inherit",
65
+ extendEnv: true,
66
+ env: {
67
+ VITE_SPEC: options.spec,
68
+ VITE_SDK_IT_OUTPUT: options.output
69
+ }
70
+ });
71
+ });
25
72
 
26
73
  // packages/cli/src/lib/langs/dart.ts
27
- var dart_default = new Command("dart").description("Generate Dart SDK").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(true)).option("-l, --language <language>", "Programming language for the SDK").option("-n, --name <name>", "Name of the generated client", "Client").option("-v, --verbose", "Verbose output", false).action(async (options) => {
28
- const spec = augmentSpec({ spec: await loadSpec(options.spec) }, true);
29
- await generate(spec, {
74
+ import { Command as Command2 } from "commander";
75
+ import { execFile, execSync } from "node:child_process";
76
+ import { generate } from "@sdk-it/dart";
77
+ import { loadSpec } from "@sdk-it/spec";
78
+ var dart_default = new Command2("dart").description("Generate Dart SDK").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(true)).option("-l, --language <language>", "Programming language for the SDK").option("-n, --name <name>", "Name of the generated client", "Client").option(
79
+ "--pagination <pagination>",
80
+ 'Configure pagination (e.g., "false", "true", "guess=false")',
81
+ "true"
82
+ ).option("-v, --verbose", "Verbose output", false).action(async (options) => {
83
+ await generate(await loadSpec(options.spec), {
30
84
  output: options.output,
31
85
  mode: options.mode || "full",
32
86
  name: options.name,
87
+ pagination: parsePagination(parseDotConfig(options.pagination ?? "true")),
33
88
  formatCode: ({ output }) => {
34
89
  if (options.formatter) {
35
90
  const [command2, ...args] = options.formatter.split(" ");
@@ -46,18 +101,2021 @@ var dart_default = new Command("dart").description("Generate Dart SDK").addOptio
46
101
  });
47
102
  });
48
103
 
104
+ // packages/cli/src/lib/langs/python.ts
105
+ import { Command as Command3 } from "commander";
106
+ import { execFile as execFile2, execSync as execSync2 } from "node:child_process";
107
+
108
+ // packages/python/dist/index.js
109
+ import { readdir } from "node:fs/promises";
110
+ import { join } from "node:path";
111
+ import { snakecase as snakecase2 } from "stringcase";
112
+ import { isEmpty, isRef as isRef2, pascalcase as pascalcase2 } from "@sdk-it/core";
113
+ import {
114
+ createWriterProxy,
115
+ writeFiles
116
+ } from "@sdk-it/core/file-system.js";
117
+ import {
118
+ augmentSpec,
119
+ cleanFiles,
120
+ forEachOperation,
121
+ isSuccessStatusCode,
122
+ parseJsonContentType,
123
+ readWriteMetadata
124
+ } from "@sdk-it/spec";
125
+ import { snakecase } from "stringcase";
126
+ import { isRef, notRef, parseRef, pascalcase } from "@sdk-it/core";
127
+ import { isPrimitiveSchema } from "@sdk-it/spec";
128
+ var dispatcher_default = `"""HTTP dispatcher for making API requests."""
129
+
130
+ import asyncio
131
+ import logging
132
+ from typing import Any, Dict, List, Optional, Union
133
+ from urllib.parse import urljoin, urlparse
134
+
135
+ import httpx
136
+ from pydantic import BaseModel
137
+
138
+ from .interceptors import Interceptor
139
+ from .responses import ApiResponse, ErrorResponse
140
+
141
+
142
+ class RequestConfig(BaseModel):
143
+ """Configuration for an HTTP request."""
144
+
145
+ method: str
146
+ url: str
147
+ headers: Optional[Dict[str, str]] = None
148
+ params: Optional[Dict[str, Any]] = None
149
+ json_data: Optional[Dict[str, Any]] = None
150
+ form_data: Optional[Dict[str, Any]] = None
151
+ files: Optional[Dict[str, Any]] = None
152
+ timeout: Optional[Union[float, httpx.Timeout]] = None
153
+
154
+ class Config:
155
+ """Pydantic configuration."""
156
+ arbitrary_types_allowed = True
157
+
158
+
159
+ class Dispatcher:
160
+ """HTTP client dispatcher with interceptor support."""
161
+
162
+ def __init__(
163
+ self,
164
+ interceptors: Optional[List[Interceptor]] = None,
165
+ client: Optional[httpx.AsyncClient] = None,
166
+ timeout: Optional[Union[float, httpx.Timeout]] = None
167
+ ):
168
+ """Initialize the dispatcher.
169
+
170
+ Args:
171
+ interceptors: List of interceptors to apply to requests/responses
172
+ client: Custom httpx.AsyncClient instance (creates default if None)
173
+ timeout: Default timeout for requests
174
+ """
175
+ self.interceptors = interceptors or []
176
+ self.client = client or httpx.AsyncClient(timeout=timeout)
177
+ self.logger = logging.getLogger(__name__)
178
+
179
+ async def __aenter__(self):
180
+ """Async context manager entry."""
181
+ return self
182
+
183
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
184
+ """Async context manager exit."""
185
+ await self.client.aclose()
186
+
187
+ async def request(self, config: RequestConfig) -> httpx.Response:
188
+ """Execute an HTTP request with interceptor processing.
189
+
190
+ Args:
191
+ config: Request configuration
192
+
193
+ Returns:
194
+ HTTP response after processing through interceptors
195
+
196
+ Raises:
197
+ httpx.HTTPError: For HTTP-related errors
198
+ ValueError: For invalid request configuration
199
+ """
200
+ # Process request interceptors
201
+ processed_config = config
202
+ for interceptor in self.interceptors:
203
+ processed_config = await interceptor.process_request(processed_config)
204
+
205
+ # Prepare request arguments
206
+ request_kwargs = self._prepare_request_kwargs(processed_config)
207
+
208
+ try:
209
+ # Execute request
210
+ response = await self.client.request(**request_kwargs)
211
+
212
+ # Process response interceptors (in reverse order)
213
+ for interceptor in reversed(self.interceptors):
214
+ response = await interceptor.process_response(response)
215
+
216
+ return response
217
+
218
+ except httpx.RequestError as e:
219
+ self.logger.error(f"Request failed: {e}")
220
+ raise
221
+ except Exception as e:
222
+ self.logger.error(f"Unexpected error during request: {e}")
223
+ raise
224
+
225
+ def _prepare_request_kwargs(self, config: RequestConfig) -> Dict[str, Any]:
226
+ """Prepare keyword arguments for httpx request.
227
+
228
+ Args:
229
+ config: Request configuration
230
+
231
+ Returns:
232
+ Dictionary of kwargs for httpx.request
233
+
234
+ Raises:
235
+ ValueError: If request configuration is invalid
236
+ """
237
+ if not config.method:
238
+ raise ValueError("Request method cannot be empty")
239
+
240
+ if not config.url:
241
+ raise ValueError("Request URL cannot be empty")
242
+
243
+ request_kwargs = {
244
+ 'method': config.method.upper(),
245
+ 'url': config.url,
246
+ 'headers': config.headers or {},
247
+ 'params': config.params,
248
+ 'timeout': config.timeout,
249
+ }
250
+
251
+ # Handle different content types
252
+ content_type_set = False
253
+
254
+ if config.json_data is not None:
255
+ request_kwargs['json'] = config.json_data
256
+ if 'Content-Type' not in request_kwargs['headers']:
257
+ request_kwargs['headers']['Content-Type'] = 'application/json'
258
+ content_type_set = True
259
+
260
+ elif config.form_data is not None:
261
+ request_kwargs['data'] = config.form_data
262
+ if 'Content-Type' not in request_kwargs['headers']:
263
+ request_kwargs['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
264
+ content_type_set = True
265
+
266
+ elif config.files is not None:
267
+ request_kwargs['files'] = config.files
268
+ # Don't set Content-Type for multipart/form-data - httpx will handle it automatically
269
+ content_type_set = True
270
+
271
+ # Validate that only one content type is set
272
+ content_fields = [config.json_data, config.form_data, config.files]
273
+ non_none_count = sum(1 for field in content_fields if field is not None)
274
+
275
+ if non_none_count > 1:
276
+ raise ValueError(
277
+ "Only one of json_data, form_data, or files can be set in a single request"
278
+ )
279
+
280
+ return request_kwargs
281
+
282
+ async def json(self, config: RequestConfig) -> httpx.Response:
283
+ """Make a JSON request.
284
+
285
+ Args:
286
+ config: Request configuration
287
+
288
+ Returns:
289
+ HTTP response
290
+ """
291
+ return await self.request(config)
292
+
293
+ async def form(self, config: RequestConfig) -> httpx.Response:
294
+ """Make a form-encoded request.
295
+
296
+ Args:
297
+ config: Request configuration
298
+
299
+ Returns:
300
+ HTTP response
301
+ """
302
+ return await self.request(config)
303
+
304
+ async def multipart(self, config: RequestConfig) -> httpx.Response:
305
+ """Make a multipart/form-data request.
306
+
307
+ Args:
308
+ config: Request configuration
309
+
310
+ Returns:
311
+ HTTP response
312
+ """
313
+ return await self.request(config)
314
+
315
+ async def close(self):
316
+ """Close the HTTP client and clean up resources."""
317
+ await self.client.aclose()
318
+
319
+
320
+ class Receiver:
321
+ """Response processor with interceptor support."""
322
+
323
+ def __init__(
324
+ self,
325
+ interceptors: Optional[List[Interceptor]] = None,
326
+ logger: Optional[logging.Logger] = None
327
+ ):
328
+ """Initialize the receiver.
329
+
330
+ Args:
331
+ interceptors: List of interceptors to apply to responses
332
+ logger: Custom logger instance
333
+ """
334
+ self.interceptors = interceptors or []
335
+ self.logger = logger or logging.getLogger(__name__)
336
+
337
+ async def json(
338
+ self,
339
+ response: httpx.Response,
340
+ success_model: Optional[type] = None,
341
+ error_model: Optional[type] = None
342
+ ) -> Any:
343
+ """Process a JSON response.
344
+
345
+ Args:
346
+ response: HTTP response to process
347
+ success_model: Pydantic model for successful responses
348
+ error_model: Pydantic model for error responses
349
+
350
+ Returns:
351
+ Parsed response data, optionally as model instances
352
+
353
+ Raises:
354
+ ErrorResponse: For HTTP error status codes
355
+ ValueError: For response parsing errors
356
+ """
357
+ # Process response interceptors
358
+ processed_response = response
359
+ for interceptor in self.interceptors:
360
+ processed_response = await interceptor.process_response(processed_response)
361
+
362
+ # Handle different status codes
363
+ if 200 <= processed_response.status_code < 300:
364
+ return await self._handle_success_response(
365
+ processed_response, success_model
366
+ )
367
+ else:
368
+ await self._handle_error_response(
369
+ processed_response, error_model
370
+ )
371
+
372
+ async def _handle_success_response(
373
+ self,
374
+ response: httpx.Response,
375
+ success_model: Optional[type] = None
376
+ ) -> Any:
377
+ """Handle successful response.
378
+
379
+ Args:
380
+ response: HTTP response
381
+ success_model: Pydantic model for successful responses
382
+
383
+ Returns:
384
+ Parsed response data
385
+
386
+ Raises:
387
+ ValueError: For parsing errors
388
+ """
389
+ if not response.content:
390
+ return None
391
+
392
+ try:
393
+ data = response.json()
394
+
395
+ if success_model:
396
+ if isinstance(data, list):
397
+ return [success_model(**item) for item in data]
398
+ else:
399
+ return success_model(**data)
400
+
401
+ return data
402
+
403
+ except Exception as e:
404
+ self.logger.error(f"Failed to parse success response: {e}")
405
+ raise ValueError(f"Failed to parse response: {e}")
406
+
407
+ async def _handle_error_response(
408
+ self,
409
+ response: httpx.Response,
410
+ error_model: Optional[type] = None
411
+ ) -> None:
412
+ """Handle error response.
413
+
414
+ Args:
415
+ response: HTTP response
416
+ error_model: Pydantic model for error responses
417
+
418
+ Raises:
419
+ ErrorResponse: Always raises with error details
420
+ """
421
+ error_data = {}
422
+
423
+ if response.content:
424
+ try:
425
+ error_data = response.json()
426
+ except Exception:
427
+ # Fallback to text content if JSON parsing fails
428
+ error_data = {'message': response.text}
429
+
430
+ if error_model:
431
+ try:
432
+ error = error_model(**error_data)
433
+ raise ErrorResponse(error, response.status_code, dict(response.headers))
434
+ except Exception as e:
435
+ self.logger.warning(f"Failed to parse error with model {error_model}: {e}")
436
+
437
+ raise ErrorResponse(error_data, response.status_code, dict(response.headers))
438
+
439
+ async def stream(self, response: httpx.Response) -> httpx.Response:
440
+ """Return streaming response as-is.
441
+
442
+ Args:
443
+ response: HTTP response
444
+
445
+ Returns:
446
+ The unmodified streaming response
447
+ """
448
+ return response
449
+
450
+ async def text(self, response: httpx.Response) -> str:
451
+ """Get response as text.
452
+
453
+ Args:
454
+ response: HTTP response
455
+
456
+ Returns:
457
+ Response body as text
458
+
459
+ Raises:
460
+ ErrorResponse: For HTTP error status codes
461
+ """
462
+ # Process response interceptors
463
+ processed_response = response
464
+ for interceptor in self.interceptors:
465
+ processed_response = await interceptor.process_response(processed_response)
466
+
467
+ if 200 <= processed_response.status_code < 300:
468
+ return processed_response.text
469
+ else:
470
+ error_data = {'message': processed_response.text}
471
+ raise ErrorResponse(error_data, processed_response.status_code, dict(processed_response.headers))
472
+
473
+ async def bytes(self, response: httpx.Response) -> bytes:
474
+ """Get response as bytes.
475
+
476
+ Args:
477
+ response: HTTP response
478
+
479
+ Returns:
480
+ Response body as bytes
481
+
482
+ Raises:
483
+ ErrorResponse: For HTTP error status codes
484
+ """
485
+ # Process response interceptors
486
+ processed_response = response
487
+ for interceptor in self.interceptors:
488
+ processed_response = await interceptor.process_response(processed_response)
489
+
490
+ if 200 <= processed_response.status_code < 300:
491
+ return processed_response.content
492
+ else:
493
+ error_data = {'message': 'Binary response error'}
494
+ raise ErrorResponse(error_data, processed_response.status_code, dict(processed_response.headers))
495
+
496
+
497
+ # Convenience functions for common use cases
498
+ async def quick_request(
499
+ method: str,
500
+ url: str,
501
+ interceptors: Optional[List[Interceptor]] = None,
502
+ **kwargs
503
+ ) -> httpx.Response:
504
+ """Make a quick HTTP request with interceptors.
505
+
506
+ Args:
507
+ method: HTTP method
508
+ url: Request URL
509
+ interceptors: List of interceptors to apply
510
+ **kwargs: Additional request configuration
511
+
512
+ Returns:
513
+ HTTP response
514
+ """
515
+ config = RequestConfig(method=method, url=url, **kwargs)
516
+
517
+ async with Dispatcher(interceptors=interceptors) as dispatcher:
518
+ return await dispatcher.request(config)
519
+
520
+
521
+ async def quick_json_request(
522
+ method: str,
523
+ url: str,
524
+ json_data: Optional[Dict[str, Any]] = None,
525
+ interceptors: Optional[List[Interceptor]] = None,
526
+ success_model: Optional[type] = None,
527
+ error_model: Optional[type] = None,
528
+ **kwargs
529
+ ) -> Any:
530
+ """Make a quick JSON HTTP request with interceptors.
531
+
532
+ Args:
533
+ method: HTTP method
534
+ url: Request URL
535
+ json_data: JSON data to send
536
+ interceptors: List of interceptors to apply
537
+ success_model: Pydantic model for successful responses
538
+ error_model: Pydantic model for error responses
539
+ **kwargs: Additional request configuration
540
+
541
+ Returns:
542
+ Parsed JSON response
543
+ """
544
+ config = RequestConfig(method=method, url=url, json_data=json_data, **kwargs)
545
+
546
+ async with Dispatcher(interceptors=interceptors) as dispatcher:
547
+ response = await dispatcher.request(config)
548
+ receiver = Receiver(interceptors=interceptors)
549
+ return await receiver.json(response, success_model, error_model)
550
+ `;
551
+ var interceptors_default = `"""HTTP interceptors for request/response processing."""
552
+
553
+ import asyncio
554
+ import logging
555
+ import time
556
+ from abc import ABC, abstractmethod
557
+ from typing import Dict, Optional, List, Any, Union
558
+ from urllib.parse import urljoin
559
+
560
+ import httpx
561
+
562
+ from .dispatcher import RequestConfig
563
+
564
+
565
+ class Interceptor(ABC):
566
+ """Base class for HTTP interceptors."""
567
+
568
+ @abstractmethod
569
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
570
+ """Process an outgoing request.
571
+
572
+ Args:
573
+ config: The request configuration to process
574
+
575
+ Returns:
576
+ The modified request configuration
577
+ """
578
+ pass
579
+
580
+ @abstractmethod
581
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
582
+ """Process an incoming response.
583
+
584
+ Args:
585
+ response: The HTTP response to process
586
+
587
+ Returns:
588
+ The processed response
589
+ """
590
+ pass
591
+
592
+
593
+ class BaseUrlInterceptor(Interceptor):
594
+ """Interceptor that prepends base URL to relative URLs."""
595
+
596
+ def __init__(self, base_url: str):
597
+ """Initialize the base URL interceptor.
598
+
599
+ Args:
600
+ base_url: The base URL to prepend to relative URLs
601
+ """
602
+ self.base_url = base_url.rstrip('/')
603
+
604
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
605
+ """Prepend base URL if the request URL is relative.
606
+
607
+ Args:
608
+ config: The request configuration
609
+
610
+ Returns:
611
+ The modified request configuration with absolute URL
612
+ """
613
+ if not config.url.startswith(('http://', 'https://')):
614
+ # Use urljoin for proper URL joining, ensuring single slash
615
+ config.url = urljoin(self.base_url + '/', config.url.lstrip('/'))
616
+ return config
617
+
618
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
619
+ """Pass through response unchanged.
620
+
621
+ Args:
622
+ response: The HTTP response
623
+
624
+ Returns:
625
+ The unmodified response
626
+ """
627
+ return response
628
+
629
+
630
+ class LoggingInterceptor(Interceptor):
631
+ """Interceptor that logs requests and responses using Python's logging module."""
632
+
633
+ def __init__(
634
+ self,
635
+ enabled: bool = True,
636
+ logger: Optional[logging.Logger] = None,
637
+ log_level: int = logging.INFO,
638
+ include_headers: bool = True,
639
+ include_sensitive_headers: bool = False
640
+ ):
641
+ """Initialize the logging interceptor.
642
+
643
+ Args:
644
+ enabled: Whether logging is enabled
645
+ logger: Custom logger instance (creates default if None)
646
+ log_level: Logging level to use
647
+ include_headers: Whether to log request/response headers
648
+ include_sensitive_headers: Whether to log sensitive headers like Authorization
649
+ """
650
+ self.enabled = enabled
651
+ self.logger = logger or logging.getLogger(__name__)
652
+ self.log_level = log_level
653
+ self.include_headers = include_headers
654
+ self.include_sensitive_headers = include_sensitive_headers
655
+ self._sensitive_headers = {'authorization', 'x-api-key', 'cookie', 'set-cookie'}
656
+
657
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
658
+ """Log outgoing request.
659
+
660
+ Args:
661
+ config: The request configuration
662
+
663
+ Returns:
664
+ The unmodified request configuration
665
+ """
666
+ if not self.enabled:
667
+ return config
668
+
669
+ self.logger.log(self.log_level, f"\u2192 {config.method.upper()} {config.url}")
670
+
671
+ if self.include_headers and config.headers:
672
+ for key, value in config.headers.items():
673
+ if (key.lower() in self._sensitive_headers and
674
+ not self.include_sensitive_headers):
675
+ self.logger.log(self.log_level, f" {key}: [REDACTED]")
676
+ else:
677
+ self.logger.log(self.log_level, f" {key}: {value}")
678
+
679
+ return config
680
+
681
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
682
+ """Log incoming response.
683
+
684
+ Args:
685
+ response: The HTTP response
686
+
687
+ Returns:
688
+ The unmodified response
689
+ """
690
+ if not self.enabled:
691
+ return response
692
+
693
+ status_icon = "\u2713" if 200 <= response.status_code < 300 else "\u2717"
694
+ self.logger.log(
695
+ self.log_level,
696
+ f"\u2190 {status_icon} {response.status_code} {response.reason_phrase or ''}"
697
+ )
698
+
699
+ if self.include_headers and response.headers:
700
+ for key, value in response.headers.items():
701
+ if (key.lower() in self._sensitive_headers and
702
+ not self.include_sensitive_headers):
703
+ self.logger.log(self.log_level, f" {key}: [REDACTED]")
704
+ else:
705
+ self.logger.log(self.log_level, f" {key}: {value}")
706
+
707
+ return response
708
+
709
+
710
+ class AuthInterceptor(Interceptor):
711
+ """Interceptor that adds authentication headers."""
712
+
713
+ def __init__(
714
+ self,
715
+ token: Optional[str] = None,
716
+ api_key: Optional[str] = None,
717
+ api_key_header: str = 'X-API-Key',
718
+ auth_type: str = 'Bearer'
719
+ ):
720
+ """Initialize the authentication interceptor.
721
+
722
+ Args:
723
+ token: Bearer token for Authorization header
724
+ api_key: API key value
725
+ api_key_header: Header name for API key
726
+ auth_type: Type of authentication (Bearer, Basic, etc.)
727
+ """
728
+ self.token = token
729
+ self.api_key = api_key
730
+ self.api_key_header = api_key_header
731
+ self.auth_type = auth_type
732
+
733
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
734
+ """Add authentication headers.
735
+
736
+ Args:
737
+ config: The request configuration
738
+
739
+ Returns:
740
+ The modified request configuration with auth headers
741
+ """
742
+ if config.headers is None:
743
+ config.headers = {}
744
+
745
+ if self.token:
746
+ config.headers['Authorization'] = f'{self.auth_type} {self.token}'
747
+ elif self.api_key:
748
+ config.headers[self.api_key_header] = self.api_key
749
+
750
+ return config
751
+
752
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
753
+ """Pass through response unchanged.
754
+
755
+ Args:
756
+ response: The HTTP response
757
+
758
+ Returns:
759
+ The unmodified response
760
+ """
761
+ return response
762
+
763
+
764
+ class RetryInterceptor(Interceptor):
765
+ """Interceptor that retries failed requests with exponential backoff."""
766
+
767
+ def __init__(
768
+ self,
769
+ max_retries: int = 3,
770
+ retry_delay: float = 1.0,
771
+ backoff_factor: float = 2.0,
772
+ retry_on_status: Optional[List[int]] = None,
773
+ retry_on_exceptions: Optional[List[type]] = None
774
+ ):
775
+ """Initialize the retry interceptor.
776
+
777
+ Args:
778
+ max_retries: Maximum number of retry attempts
779
+ retry_delay: Initial delay between retries in seconds
780
+ backoff_factor: Exponential backoff multiplier
781
+ retry_on_status: HTTP status codes that should trigger retries
782
+ retry_on_exceptions: Exception types that should trigger retries
783
+ """
784
+ self.max_retries = max_retries
785
+ self.retry_delay = retry_delay
786
+ self.backoff_factor = backoff_factor
787
+ self.retry_on_status = retry_on_status or [500, 502, 503, 504, 408, 429]
788
+ self.retry_on_exceptions = retry_on_exceptions or [
789
+ httpx.TimeoutException,
790
+ httpx.ConnectError,
791
+ httpx.RemoteProtocolError
792
+ ]
793
+ self._original_request_func = None
794
+ self.logger = logging.getLogger(__name__)
795
+
796
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
797
+ """Store original request for potential retries.
798
+
799
+ Args:
800
+ config: The request configuration
801
+
802
+ Returns:
803
+ The unmodified request configuration
804
+ """
805
+ # Store the original config for retries
806
+ self._original_config = config.model_copy() if hasattr(config, 'model_copy') else config
807
+ return config
808
+
809
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
810
+ """Check if response needs retry and handle accordingly.
811
+
812
+ Args:
813
+ response: The HTTP response
814
+
815
+ Returns:
816
+ The response (possibly after retries)
817
+ """
818
+ # For retry logic to work properly, it needs to be integrated at the dispatcher level
819
+ # This is a simplified version that just passes through
820
+ # In a full implementation, the retry logic would need access to the original request method
821
+ return response
822
+
823
+ async def execute_with_retry(self, request_func, *args, **kwargs) -> httpx.Response:
824
+ """Execute a request function with retry logic.
825
+
826
+ Args:
827
+ request_func: Function that executes the HTTP request
828
+ *args: Arguments to pass to request_func
829
+ **kwargs: Keyword arguments to pass to request_func
830
+
831
+ Returns:
832
+ The HTTP response after potential retries
833
+
834
+ Raises:
835
+ The last exception encountered if all retries fail
836
+ """
837
+ last_exception = None
838
+
839
+ for attempt in range(self.max_retries + 1):
840
+ try:
841
+ response = await request_func(*args, **kwargs)
842
+
843
+ # Check if response status requires retry
844
+ if response.status_code not in self.retry_on_status:
845
+ return response
846
+
847
+ if attempt == self.max_retries:
848
+ self.logger.warning(
849
+ f"Max retries ({self.max_retries}) reached for request. "
850
+ f"Final status: {response.status_code}"
851
+ )
852
+ return response
853
+
854
+ # Wait before retry
855
+ delay = self.retry_delay * (self.backoff_factor ** attempt)
856
+ self.logger.info(
857
+ f"Retrying request (attempt {attempt + 1}/{self.max_retries + 1}) "
858
+ f"after {delay:.2f}s due to status {response.status_code}"
859
+ )
860
+ await asyncio.sleep(delay)
861
+
862
+ except Exception as e:
863
+ # Check if exception type requires retry
864
+ if not any(isinstance(e, exc_type) for exc_type in self.retry_on_exceptions):
865
+ raise e
866
+
867
+ last_exception = e
868
+
869
+ if attempt == self.max_retries:
870
+ self.logger.error(
871
+ f"Max retries ({self.max_retries}) reached. "
872
+ f"Final exception: {type(e).__name__}: {e}"
873
+ )
874
+ raise e
875
+
876
+ # Wait before retry
877
+ delay = self.retry_delay * (self.backoff_factor ** attempt)
878
+ self.logger.info(
879
+ f"Retrying request (attempt {attempt + 1}/{self.max_retries + 1}) "
880
+ f"after {delay:.2f}s due to {type(e).__name__}: {e}"
881
+ )
882
+ await asyncio.sleep(delay)
883
+
884
+
885
+ class UserAgentInterceptor(Interceptor):
886
+ """Interceptor that adds a User-Agent header."""
887
+
888
+ def __init__(self, user_agent: str):
889
+ """Initialize the User-Agent interceptor.
890
+
891
+ Args:
892
+ user_agent: The User-Agent string to set
893
+ """
894
+ self.user_agent = user_agent
895
+
896
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
897
+ """Add User-Agent header if not already present.
898
+
899
+ Args:
900
+ config: The request configuration
901
+
902
+ Returns:
903
+ The modified request configuration with User-Agent header
904
+ """
905
+ if config.headers is None:
906
+ config.headers = {}
907
+
908
+ # Only set User-Agent if not already present (case-insensitive check)
909
+ has_user_agent = any(
910
+ key.lower() == 'user-agent'
911
+ for key in config.headers.keys()
912
+ )
913
+
914
+ if not has_user_agent:
915
+ config.headers['User-Agent'] = self.user_agent
916
+
917
+ return config
918
+
919
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
920
+ """Pass through response unchanged.
921
+
922
+ Args:
923
+ response: The HTTP response
924
+
925
+ Returns:
926
+ The unmodified response
927
+ """
928
+ return response
929
+
930
+
931
+ class TimeoutInterceptor(Interceptor):
932
+ """Interceptor that sets request timeouts."""
933
+
934
+ def __init__(self, timeout: Union[float, httpx.Timeout]):
935
+ """Initialize the timeout interceptor.
936
+
937
+ Args:
938
+ timeout: Timeout value in seconds or httpx.Timeout object
939
+ """
940
+ self.timeout = timeout
941
+
942
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
943
+ """Set timeout for the request.
944
+
945
+ Args:
946
+ config: The request configuration
947
+
948
+ Returns:
949
+ The modified request configuration with timeout
950
+ """
951
+ if config.timeout is None:
952
+ config.timeout = self.timeout
953
+ return config
954
+
955
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
956
+ """Pass through response unchanged.
957
+
958
+ Args:
959
+ response: The HTTP response
960
+
961
+ Returns:
962
+ The unmodified response
963
+ """
964
+ return response
965
+
966
+
967
+ class RateLimitInterceptor(Interceptor):
968
+ """Interceptor that implements client-side rate limiting."""
969
+
970
+ def __init__(self, max_requests: int, time_window: float = 60.0):
971
+ """Initialize the rate limit interceptor.
972
+
973
+ Args:
974
+ max_requests: Maximum number of requests allowed in the time window
975
+ time_window: Time window in seconds
976
+ """
977
+ self.max_requests = max_requests
978
+ self.time_window = time_window
979
+ self.requests = []
980
+ self._lock = asyncio.Lock()
981
+
982
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
983
+ """Apply rate limiting before request.
984
+
985
+ Args:
986
+ config: The request configuration
987
+
988
+ Returns:
989
+ The unmodified request configuration
990
+ """
991
+ async with self._lock:
992
+ now = time.time()
993
+
994
+ # Remove requests outside the time window
995
+ self.requests = [req_time for req_time in self.requests
996
+ if now - req_time < self.time_window]
997
+
998
+ # Check if we've exceeded the rate limit
999
+ if len(self.requests) >= self.max_requests:
1000
+ # Calculate how long to wait
1001
+ oldest_request = min(self.requests)
1002
+ wait_time = self.time_window - (now - oldest_request)
1003
+
1004
+ if wait_time > 0:
1005
+ await asyncio.sleep(wait_time)
1006
+
1007
+ # Record this request
1008
+ self.requests.append(now)
1009
+
1010
+ return config
1011
+
1012
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
1013
+ """Pass through response unchanged.
1014
+
1015
+ Args:
1016
+ response: The HTTP response
1017
+
1018
+ Returns:
1019
+ The unmodified response
1020
+ """
1021
+ return response
1022
+
1023
+
1024
+ # Factory functions for convenient interceptor creation
1025
+ def create_base_url_interceptor(base_url: str) -> BaseUrlInterceptor:
1026
+ """Create a BaseUrlInterceptor instance.
1027
+
1028
+ Args:
1029
+ base_url: The base URL to prepend to relative URLs
1030
+
1031
+ Returns:
1032
+ Configured BaseUrlInterceptor instance
1033
+ """
1034
+ return BaseUrlInterceptor(base_url)
1035
+
1036
+
1037
+ def create_logging_interceptor(
1038
+ enabled: bool = True,
1039
+ log_level: int = logging.INFO,
1040
+ include_headers: bool = True,
1041
+ include_sensitive_headers: bool = False
1042
+ ) -> LoggingInterceptor:
1043
+ """Create a LoggingInterceptor instance.
1044
+
1045
+ Args:
1046
+ enabled: Whether logging is enabled
1047
+ log_level: Logging level to use
1048
+ include_headers: Whether to log headers
1049
+ include_sensitive_headers: Whether to log sensitive headers
1050
+
1051
+ Returns:
1052
+ Configured LoggingInterceptor instance
1053
+ """
1054
+ return LoggingInterceptor(
1055
+ enabled=enabled,
1056
+ log_level=log_level,
1057
+ include_headers=include_headers,
1058
+ include_sensitive_headers=include_sensitive_headers
1059
+ )
1060
+
1061
+
1062
+ def create_auth_interceptor(
1063
+ token: Optional[str] = None,
1064
+ api_key: Optional[str] = None,
1065
+ api_key_header: str = 'X-API-Key',
1066
+ auth_type: str = 'Bearer'
1067
+ ) -> AuthInterceptor:
1068
+ """Create an AuthInterceptor instance.
1069
+
1070
+ Args:
1071
+ token: Bearer token for Authorization header
1072
+ api_key: API key value
1073
+ api_key_header: Header name for API key
1074
+ auth_type: Type of authentication
1075
+
1076
+ Returns:
1077
+ Configured AuthInterceptor instance
1078
+ """
1079
+ return AuthInterceptor(
1080
+ token=token,
1081
+ api_key=api_key,
1082
+ api_key_header=api_key_header,
1083
+ auth_type=auth_type
1084
+ )
1085
+
1086
+
1087
+ def create_retry_interceptor(
1088
+ max_retries: int = 3,
1089
+ retry_delay: float = 1.0,
1090
+ backoff_factor: float = 2.0,
1091
+ retry_on_status: Optional[List[int]] = None
1092
+ ) -> RetryInterceptor:
1093
+ """Create a RetryInterceptor instance.
1094
+
1095
+ Args:
1096
+ max_retries: Maximum number of retry attempts
1097
+ retry_delay: Initial delay between retries in seconds
1098
+ backoff_factor: Exponential backoff multiplier
1099
+ retry_on_status: HTTP status codes that should trigger retries
1100
+
1101
+ Returns:
1102
+ Configured RetryInterceptor instance
1103
+ """
1104
+ return RetryInterceptor(
1105
+ max_retries=max_retries,
1106
+ retry_delay=retry_delay,
1107
+ backoff_factor=backoff_factor,
1108
+ retry_on_status=retry_on_status
1109
+ )
1110
+
1111
+
1112
+ def create_user_agent_interceptor(user_agent: str) -> UserAgentInterceptor:
1113
+ """Create a UserAgentInterceptor instance.
1114
+
1115
+ Args:
1116
+ user_agent: The User-Agent string to set
1117
+
1118
+ Returns:
1119
+ Configured UserAgentInterceptor instance
1120
+ """
1121
+ return UserAgentInterceptor(user_agent)
1122
+ `;
1123
+ var responses_default = `"""HTTP response models and exceptions."""
1124
+
1125
+ from typing import Any, Dict, Optional, Union
1126
+
1127
+ import httpx
1128
+ from pydantic import BaseModel
1129
+
1130
+
1131
+ class ApiResponse(BaseModel):
1132
+ """Base class for API responses."""
1133
+
1134
+ status_code: int
1135
+ headers: Dict[str, str]
1136
+ data: Any
1137
+
1138
+ class Config:
1139
+ """Pydantic configuration."""
1140
+ arbitrary_types_allowed = True
1141
+
1142
+
1143
+ class SuccessResponse(ApiResponse):
1144
+ """Represents a successful API response."""
1145
+
1146
+ def __init__(self, data: Any, status_code: int = 200, headers: Optional[Dict[str, str]] = None):
1147
+ """Initialize success response.
1148
+
1149
+ Args:
1150
+ data: Response data
1151
+ status_code: HTTP status code
1152
+ headers: Response headers
1153
+ """
1154
+ super().__init__(
1155
+ status_code=status_code,
1156
+ headers=headers or {},
1157
+ data=data
1158
+ )
1159
+
1160
+
1161
+ class ErrorResponse(Exception):
1162
+ """Exception raised for HTTP error responses."""
1163
+
1164
+ def __init__(
1165
+ self,
1166
+ data: Any,
1167
+ status_code: int,
1168
+ headers: Optional[Dict[str, str]] = None,
1169
+ message: Optional[str] = None
1170
+ ):
1171
+ """Initialize error response.
1172
+
1173
+ Args:
1174
+ data: Error response data
1175
+ status_code: HTTP status code
1176
+ headers: Response headers
1177
+ message: Custom error message
1178
+ """
1179
+ self.data = data
1180
+ self.status_code = status_code
1181
+ self.headers = headers or {}
1182
+ self.message = message or f"HTTP {status_code} Error"
1183
+
1184
+ super().__init__(self.message)
1185
+
1186
+ def __str__(self) -> str:
1187
+ """String representation of the error."""
1188
+ return f"ErrorResponse(status_code={self.status_code}, message='{self.message}')"
1189
+
1190
+ def __repr__(self) -> str:
1191
+ """Detailed string representation of the error."""
1192
+ return (
1193
+ f"ErrorResponse(status_code={self.status_code}, "
1194
+ f"message='{self.message}', data={self.data})"
1195
+ )
1196
+
1197
+
1198
+ class TimeoutError(ErrorResponse):
1199
+ """Exception raised for request timeouts."""
1200
+
1201
+ def __init__(self, message: str = "Request timed out"):
1202
+ """Initialize timeout error.
1203
+
1204
+ Args:
1205
+ message: Error message
1206
+ """
1207
+ super().__init__(
1208
+ data={'error': 'timeout'},
1209
+ status_code=408,
1210
+ message=message
1211
+ )
1212
+
1213
+
1214
+ class ConnectionError(ErrorResponse):
1215
+ """Exception raised for connection errors."""
1216
+
1217
+ def __init__(self, message: str = "Connection failed"):
1218
+ """Initialize connection error.
1219
+
1220
+ Args:
1221
+ message: Error message
1222
+ """
1223
+ super().__init__(
1224
+ data={'error': 'connection'},
1225
+ status_code=503,
1226
+ message=message
1227
+ )
1228
+
1229
+
1230
+ class BadRequestError(ErrorResponse):
1231
+ """Exception raised for 400 Bad Request errors."""
1232
+
1233
+ def __init__(self, data: Any = None, message: str = "Bad Request"):
1234
+ """Initialize bad request error.
1235
+
1236
+ Args:
1237
+ data: Error data
1238
+ message: Error message
1239
+ """
1240
+ super().__init__(
1241
+ data=data or {'error': 'bad_request'},
1242
+ status_code=400,
1243
+ message=message
1244
+ )
1245
+
1246
+
1247
+ class UnauthorizedError(ErrorResponse):
1248
+ """Exception raised for 401 Unauthorized errors."""
1249
+
1250
+ def __init__(self, data: Any = None, message: str = "Unauthorized"):
1251
+ """Initialize unauthorized error.
1252
+
1253
+ Args:
1254
+ data: Error data
1255
+ message: Error message
1256
+ """
1257
+ super().__init__(
1258
+ data=data or {'error': 'unauthorized'},
1259
+ status_code=401,
1260
+ message=message
1261
+ )
1262
+
1263
+
1264
+ class ForbiddenError(ErrorResponse):
1265
+ """Exception raised for 403 Forbidden errors."""
1266
+
1267
+ def __init__(self, data: Any = None, message: str = "Forbidden"):
1268
+ """Initialize forbidden error.
1269
+
1270
+ Args:
1271
+ data: Error data
1272
+ message: Error message
1273
+ """
1274
+ super().__init__(
1275
+ data=data or {'error': 'forbidden'},
1276
+ status_code=403,
1277
+ message=message
1278
+ )
1279
+
1280
+
1281
+ class NotFoundError(ErrorResponse):
1282
+ """Exception raised for 404 Not Found errors."""
1283
+
1284
+ def __init__(self, data: Any = None, message: str = "Not Found"):
1285
+ """Initialize not found error.
1286
+
1287
+ Args:
1288
+ data: Error data
1289
+ message: Error message
1290
+ """
1291
+ super().__init__(
1292
+ data=data or {'error': 'not_found'},
1293
+ status_code=404,
1294
+ message=message
1295
+ )
1296
+
1297
+
1298
+ class InternalServerError(ErrorResponse):
1299
+ """Exception raised for 500 Internal Server Error."""
1300
+
1301
+ def __init__(self, data: Any = None, message: str = "Internal Server Error"):
1302
+ """Initialize internal server error.
1303
+
1304
+ Args:
1305
+ data: Error data
1306
+ message: Error message
1307
+ """
1308
+ super().__init__(
1309
+ data=data or {'error': 'internal_server_error'},
1310
+ status_code=500,
1311
+ message=message
1312
+ )
1313
+
1314
+
1315
+ def create_error_from_response(response: httpx.Response) -> ErrorResponse:
1316
+ """Create appropriate error exception from HTTP response.
1317
+
1318
+ Args:
1319
+ response: HTTP response
1320
+
1321
+ Returns:
1322
+ Appropriate error exception
1323
+ """
1324
+ status_code = response.status_code
1325
+ headers = dict(response.headers)
1326
+
1327
+ # Try to parse error data
1328
+ try:
1329
+ data = response.json()
1330
+ except Exception:
1331
+ data = {'message': response.text}
1332
+
1333
+ # Create specific error types based on status code
1334
+ error_classes = {
1335
+ 400: BadRequestError,
1336
+ 401: UnauthorizedError,
1337
+ 403: ForbiddenError,
1338
+ 404: NotFoundError,
1339
+ 500: InternalServerError,
1340
+ }
1341
+
1342
+ error_class = error_classes.get(status_code, ErrorResponse)
1343
+
1344
+ if error_class == ErrorResponse:
1345
+ return ErrorResponse(data, status_code, headers)
1346
+ else:
1347
+ return error_class(data)
1348
+ `;
1349
+ function coerceObject(schema) {
1350
+ schema = structuredClone(schema);
1351
+ if (schema["x-properties"]) {
1352
+ schema.properties = {
1353
+ ...schema.properties ?? {},
1354
+ ...schema["x-properties"] ?? {}
1355
+ };
1356
+ }
1357
+ if (schema["x-required"]) {
1358
+ schema.required = Array.from(
1359
+ /* @__PURE__ */ new Set([
1360
+ ...Array.isArray(schema.required) ? schema.required : [],
1361
+ ...schema["x-required"] || []
1362
+ ])
1363
+ );
1364
+ }
1365
+ return schema;
1366
+ }
1367
+ var PythonEmitter = class {
1368
+ #spec;
1369
+ #emitHandler;
1370
+ #emitHistory = /* @__PURE__ */ new Set();
1371
+ #typeCache = /* @__PURE__ */ new Map();
1372
+ // Cache for resolved types
1373
+ #emit(name, content, schema) {
1374
+ if (this.#emitHistory.has(content)) {
1375
+ return;
1376
+ }
1377
+ this.#emitHistory.add(content);
1378
+ this.#emitHandler?.(name, content, schema);
1379
+ }
1380
+ constructor(spec) {
1381
+ this.#spec = spec;
1382
+ }
1383
+ onEmit(emit) {
1384
+ this.#emitHandler = emit;
1385
+ }
1386
+ #formatFieldName(name) {
1387
+ let fieldName = snakecase(name);
1388
+ const reservedKeywords = [
1389
+ "class",
1390
+ "def",
1391
+ "if",
1392
+ "else",
1393
+ "elif",
1394
+ "while",
1395
+ "for",
1396
+ "try",
1397
+ "except",
1398
+ "finally",
1399
+ "with",
1400
+ "as",
1401
+ "import",
1402
+ "from",
1403
+ "global",
1404
+ "nonlocal",
1405
+ "lambda",
1406
+ "yield",
1407
+ "return",
1408
+ "pass",
1409
+ "break",
1410
+ "continue",
1411
+ "True",
1412
+ "False",
1413
+ "None",
1414
+ "and",
1415
+ "or",
1416
+ "not",
1417
+ "in",
1418
+ "is"
1419
+ ];
1420
+ if (reservedKeywords.includes(fieldName)) {
1421
+ fieldName = `${fieldName}_`;
1422
+ }
1423
+ return fieldName;
1424
+ }
1425
+ #ref(ref) {
1426
+ const cacheKey = ref.$ref;
1427
+ if (this.#typeCache.has(cacheKey)) {
1428
+ return this.#typeCache.get(cacheKey);
1429
+ }
1430
+ const refInfo = parseRef(ref.$ref);
1431
+ const refName = refInfo.model;
1432
+ const className = pascalcase(refName);
1433
+ const result = {
1434
+ type: className,
1435
+ content: "",
1436
+ use: className,
1437
+ fromJson: `${className}.parse_obj`,
1438
+ simple: false
1439
+ };
1440
+ this.#typeCache.set(cacheKey, result);
1441
+ return result;
1442
+ }
1443
+ #oneOf(variants, context) {
1444
+ const variantTypes = variants.map((variant) => this.handle(variant, context)).map((result) => result.type || "Any").filter((type, index, arr) => arr.indexOf(type) === index);
1445
+ if (variantTypes.length === 0) {
1446
+ return {
1447
+ type: "Any",
1448
+ content: "",
1449
+ use: "Any",
1450
+ fromJson: "Any",
1451
+ simple: true
1452
+ };
1453
+ }
1454
+ if (variantTypes.length === 1) {
1455
+ return {
1456
+ type: variantTypes[0],
1457
+ content: "",
1458
+ use: variantTypes[0],
1459
+ fromJson: variantTypes[0],
1460
+ simple: true
1461
+ };
1462
+ }
1463
+ const unionType = `Union[${variantTypes.join(", ")}]`;
1464
+ return {
1465
+ type: unionType,
1466
+ content: "",
1467
+ use: unionType,
1468
+ fromJson: unionType,
1469
+ simple: true
1470
+ };
1471
+ }
1472
+ #object(className, schema, context) {
1473
+ const { properties = {}, required = [] } = coerceObject(schema);
1474
+ const fields = [];
1475
+ let baseClass = "BaseModel";
1476
+ if (schema.allOf) {
1477
+ const bases = schema.allOf.filter(notRef).map((s) => this.handle(s, context)).filter((result) => result.type).map((result) => result.type);
1478
+ if (bases.length > 0 && bases[0]) {
1479
+ baseClass = bases[0];
1480
+ }
1481
+ }
1482
+ for (const [propName, propSchema] of Object.entries(properties)) {
1483
+ if (isRef(propSchema)) {
1484
+ const refResult = this.#ref(propSchema);
1485
+ const refInfo = parseRef(propSchema.$ref);
1486
+ const refName = refInfo.model;
1487
+ const pythonType = pascalcase(refName);
1488
+ const fieldName = this.#formatFieldName(propName);
1489
+ const isRequired = required.includes(propName);
1490
+ const fieldType = isRequired ? pythonType : `Optional[${pythonType}]`;
1491
+ const defaultValue = isRequired ? "" : " = None";
1492
+ fields.push(` ${fieldName}: ${fieldType}${defaultValue}`);
1493
+ } else {
1494
+ const result = this.handle(propSchema, { ...context, name: propName });
1495
+ const fieldName = this.#formatFieldName(propName);
1496
+ const isRequired = required.includes(propName);
1497
+ let fieldType = result.type || "Any";
1498
+ if (!isRequired) {
1499
+ fieldType = `Optional[${fieldType}]`;
1500
+ }
1501
+ const defaultValue = isRequired ? "" : " = None";
1502
+ let fieldDef = ` ${fieldName}: ${fieldType}${defaultValue}`;
1503
+ if (fieldName !== propName) {
1504
+ fieldDef = ` ${fieldName}: ${fieldType} = Field(alias='${propName}'${defaultValue ? ", default=None" : ""})`;
1505
+ }
1506
+ if (propSchema.description) {
1507
+ fieldDef += ` # ${propSchema.description}`;
1508
+ }
1509
+ fields.push(fieldDef);
1510
+ }
1511
+ }
1512
+ if (schema.oneOf || schema.anyOf) {
1513
+ const unionResult = this.#oneOf(
1514
+ schema.oneOf || schema.anyOf || [],
1515
+ context
1516
+ );
1517
+ fields.push(` value: ${unionResult.type}`);
1518
+ }
1519
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
1520
+ const addlResult = this.handle(schema.additionalProperties, context);
1521
+ fields.push(
1522
+ ` additional_properties: Optional[Dict[str, ${addlResult.type || "Any"}]] = None`
1523
+ );
1524
+ }
1525
+ const docstring = schema.description ? ` """${schema.description}"""
1526
+ ` : "";
1527
+ let requestConfigMethod = "";
1528
+ if (schema["x-inputname"]) {
1529
+ requestConfigMethod = `
1530
+ def to_request_config(self, config: RequestConfig) -> RequestConfig:
1531
+ """Convert this input model to request configuration."""
1532
+ # Handle path parameters
1533
+ path_params = {}
1534
+ for key, value in self.dict(exclude_none=True).items():
1535
+ if key in config.url:
1536
+ path_params[key] = str(value)
1537
+ config.url = config.url.replace(f'{{{key}}}', str(value))
1538
+
1539
+ # Handle query parameters
1540
+ query_params = {k: v for k, v in self.dict(exclude_none=True).items()
1541
+ if k not in path_params}
1542
+ if query_params:
1543
+ config.params = query_params
1544
+
1545
+ return config
1546
+ `;
1547
+ }
1548
+ const content = `class ${className}(${baseClass}):
1549
+ ${docstring}${fields.length > 0 ? fields.join("\n") : " pass"}${requestConfigMethod}
1550
+ `;
1551
+ this.#emit(className, content, schema);
1552
+ return {
1553
+ type: className,
1554
+ content,
1555
+ use: className,
1556
+ fromJson: `${className}.parse_obj`,
1557
+ simple: false
1558
+ };
1559
+ }
1560
+ #primitive(schema, context) {
1561
+ const { type, format } = schema;
1562
+ const nullable = schema.nullable;
1563
+ let pythonType = "Any";
1564
+ switch (type) {
1565
+ case "string":
1566
+ if (format === "date-time") {
1567
+ pythonType = "datetime";
1568
+ } else if (format === "date") {
1569
+ pythonType = "date";
1570
+ } else if (format === "uuid") {
1571
+ pythonType = "UUID";
1572
+ } else if (format === "binary" || format === "byte") {
1573
+ pythonType = "bytes";
1574
+ } else {
1575
+ pythonType = "str";
1576
+ }
1577
+ break;
1578
+ case "integer":
1579
+ if (format === "int64") {
1580
+ pythonType = "int";
1581
+ } else {
1582
+ pythonType = "int";
1583
+ }
1584
+ break;
1585
+ case "number":
1586
+ pythonType = "float";
1587
+ break;
1588
+ case "boolean":
1589
+ pythonType = "bool";
1590
+ break;
1591
+ default:
1592
+ pythonType = "Any";
1593
+ }
1594
+ if (nullable) {
1595
+ pythonType = `Optional[${pythonType}]`;
1596
+ }
1597
+ return {
1598
+ type: pythonType,
1599
+ content: "",
1600
+ use: pythonType,
1601
+ fromJson: pythonType,
1602
+ simple: true,
1603
+ nullable
1604
+ };
1605
+ }
1606
+ #array(schema, context) {
1607
+ const itemsSchema = schema.items;
1608
+ if (!itemsSchema) {
1609
+ return {
1610
+ type: "List[Any]",
1611
+ content: "",
1612
+ use: "List[Any]",
1613
+ fromJson: "list",
1614
+ simple: true
1615
+ };
1616
+ }
1617
+ const itemsResult = this.handle(itemsSchema, context);
1618
+ const listType = `List[${itemsResult.type || "Any"}]`;
1619
+ return {
1620
+ type: listType,
1621
+ content: itemsResult.content,
1622
+ use: listType,
1623
+ fromJson: `List[${itemsResult.fromJson || itemsResult.type}]`,
1624
+ simple: true
1625
+ };
1626
+ }
1627
+ #enum(schema, context) {
1628
+ const { enum: enumValues } = schema;
1629
+ if (!enumValues || enumValues.length === 0) {
1630
+ return this.#primitive(schema, context);
1631
+ }
1632
+ if (!context.name) {
1633
+ throw new Error("Enum schemas must have a name in context");
1634
+ }
1635
+ const className = pascalcase(context.name);
1636
+ const enumItems = enumValues.map((value, index) => {
1637
+ const name = typeof value === "string" ? value.toUpperCase().replace(/[^A-Z0-9]/g, "_") : `VALUE_${index}`;
1638
+ const pythonValue = typeof value === "string" ? `'${value}'` : String(value);
1639
+ return ` ${name} = ${pythonValue}`;
1640
+ });
1641
+ const content = `class ${className}(Enum):
1642
+ """Enumeration for ${context.name}."""
1643
+ ${enumItems.join("\n")}
1644
+ `;
1645
+ this.#emit(className, content, schema);
1646
+ return {
1647
+ type: className,
1648
+ content,
1649
+ use: className,
1650
+ fromJson: className,
1651
+ simple: false
1652
+ };
1653
+ }
1654
+ #const(schema, context) {
1655
+ const { const: constValue } = schema;
1656
+ if (typeof constValue === "string") {
1657
+ return {
1658
+ type: `Literal['${constValue}']`,
1659
+ content: "",
1660
+ use: `Literal['${constValue}']`,
1661
+ fromJson: `'${constValue}'`,
1662
+ simple: true,
1663
+ literal: constValue
1664
+ };
1665
+ }
1666
+ return {
1667
+ type: `Literal[${JSON.stringify(constValue)}]`,
1668
+ content: "",
1669
+ use: `Literal[${JSON.stringify(constValue)}]`,
1670
+ fromJson: JSON.stringify(constValue),
1671
+ simple: true,
1672
+ literal: constValue
1673
+ };
1674
+ }
1675
+ handle(schema, context = {}) {
1676
+ if (isRef(schema)) {
1677
+ return this.#ref(schema);
1678
+ }
1679
+ if ("const" in schema && schema.const !== void 0) {
1680
+ return this.#const(schema, context);
1681
+ }
1682
+ if (schema.enum) {
1683
+ return this.#enum(schema, context);
1684
+ }
1685
+ if (schema.type === "array") {
1686
+ return this.#array(schema, context);
1687
+ }
1688
+ if (schema.oneOf || schema.anyOf) {
1689
+ return this.#oneOf(schema.oneOf || schema.anyOf || [], context);
1690
+ }
1691
+ if (schema.type === "object" || schema.properties || schema.allOf || schema.oneOf || schema.anyOf) {
1692
+ if (!context.name) {
1693
+ throw new Error("Object schemas must have a name in context");
1694
+ }
1695
+ const className = pascalcase(context.name);
1696
+ return this.#object(className, schema, context);
1697
+ }
1698
+ if (isPrimitiveSchema(schema)) {
1699
+ return this.#primitive(schema, context);
1700
+ }
1701
+ return {
1702
+ type: "Any",
1703
+ content: "",
1704
+ use: "Any",
1705
+ fromJson: "Any",
1706
+ simple: true
1707
+ };
1708
+ }
1709
+ };
1710
+ async function generate2(openapi, settings) {
1711
+ const spec = augmentSpec({ spec: openapi }, true);
1712
+ const clientName = settings.name || "Client";
1713
+ const output = settings.output;
1714
+ const { writer, files: writtenFiles } = createWriterProxy(
1715
+ settings.writer ?? writeFiles,
1716
+ settings.output
1717
+ );
1718
+ settings.writer = writer;
1719
+ settings.readFolder ??= async (folder) => {
1720
+ const files = await readdir(folder, { withFileTypes: true });
1721
+ return files.map((file) => ({
1722
+ fileName: file.name,
1723
+ filePath: join(file.parentPath, file.name),
1724
+ isFolder: file.isDirectory()
1725
+ }));
1726
+ };
1727
+ const groups = {};
1728
+ forEachOperation(spec, (entry, operation) => {
1729
+ console.log(`Processing ${entry.method} ${entry.path}`);
1730
+ const group = groups[entry.groupName] ??= {
1731
+ className: `${pascalcase2(entry.groupName)}Api`,
1732
+ methods: []
1733
+ };
1734
+ const input = toInputs(spec, { entry, operation });
1735
+ const response = toOutput(spec, operation);
1736
+ const methodName = snakecase2(
1737
+ operation.operationId || `${entry.method}_${entry.path.replace(/[^a-zA-Z0-9]/g, "_")}`
1738
+ );
1739
+ const returnType = response ? response.returnType : "httpx.Response";
1740
+ const docstring = operation.summary || operation.description ? ` """${operation.summary || operation.description}"""` : "";
1741
+ group.methods.push(`
1742
+ async def ${methodName}(self${input.haveInput ? `, input_data: ${input.inputName}` : ""}) -> ${returnType}:
1743
+ ${docstring}
1744
+ config = RequestConfig(
1745
+ method='${entry.method.toUpperCase()}',
1746
+ url='${entry.path}',
1747
+ )
1748
+
1749
+ ${input.haveInput ? "config = input_data.to_request_config(config)" : ""}
1750
+
1751
+ response = await self.dispatcher.${input.contentType}(config)
1752
+ ${response ? `return await self.receiver.json(response, ${response.successModel || "None"}, ${response.errorModel || "None"})` : "return response"}
1753
+ `);
1754
+ });
1755
+ const emitter = new PythonEmitter(spec);
1756
+ const models = await serializeModels(spec, emitter);
1757
+ const apiClasses = Object.entries(groups).reduce(
1758
+ (acc, [name, { className, methods }]) => {
1759
+ const fileName = `api/${snakecase2(name)}_api.py`;
1760
+ const imports = [
1761
+ "from typing import Optional",
1762
+ "import httpx",
1763
+ "",
1764
+ "from ..http.dispatcher import Dispatcher, RequestConfig",
1765
+ "from ..http.responses import Receiver",
1766
+ "from ..inputs import *",
1767
+ "from ..outputs import *",
1768
+ "from ..models import *",
1769
+ ""
1770
+ ].join("\n");
1771
+ acc[fileName] = `${imports}
1772
+ class ${className}:
1773
+ """API client for ${name} operations."""
1774
+
1775
+ def __init__(self, dispatcher: Dispatcher, receiver: Receiver):
1776
+ self.dispatcher = dispatcher
1777
+ self.receiver = receiver
1778
+ ${methods.join("\n")}
1779
+ `;
1780
+ return acc;
1781
+ },
1782
+ {}
1783
+ );
1784
+ const apiImports = Object.keys(groups).map(
1785
+ (name) => `from .api.${snakecase2(name)}_api import ${pascalcase2(name)}Api`
1786
+ ).join("\\n");
1787
+ const apiProperties = Object.keys(groups).map(
1788
+ (name) => ` self.${snakecase2(name)} = ${pascalcase2(name)}Api(dispatcher, receiver)`
1789
+ ).join("\\n");
1790
+ const clientCode = `"""Main API client."""
1791
+
1792
+ from typing import Optional, List
1793
+ import httpx
1794
+
1795
+ ${apiImports}
1796
+ from .http.dispatcher import Dispatcher, RequestConfig
1797
+ from .http.responses import Receiver
1798
+ from .http.interceptors import (
1799
+ Interceptor,
1800
+ BaseUrlInterceptor,
1801
+ LoggingInterceptor,
1802
+ AuthInterceptor,
1803
+ UserAgentInterceptor,
1804
+ )
1805
+
1806
+
1807
+ class ${clientName}:
1808
+ """Main API client for the SDK."""
1809
+
1810
+ def __init__(
1811
+ self,
1812
+ base_url: str,
1813
+ token: Optional[str] = None,
1814
+ api_key: Optional[str] = None,
1815
+ api_key_header: str = 'X-API-Key',
1816
+ enable_logging: bool = False,
1817
+ user_agent: Optional[str] = None,
1818
+ custom_interceptors: Optional[List[Interceptor]] = None,
1819
+ ):
1820
+ """
1821
+ Initialize the API client.
1822
+
1823
+ Args:
1824
+ base_url: Base URL for the API
1825
+ token: Bearer token for authentication
1826
+ api_key: API key for authentication
1827
+ api_key_header: Header name for API key authentication
1828
+ enable_logging: Enable request/response logging
1829
+ user_agent: Custom User-Agent header
1830
+ custom_interceptors: Additional custom interceptors
1831
+ """
1832
+ self.base_url = base_url
1833
+
1834
+ # Build interceptor chain
1835
+ interceptors = []
1836
+
1837
+ # Base URL interceptor (always first)
1838
+ interceptors.append(BaseUrlInterceptor(base_url))
1839
+
1840
+ # Authentication interceptor
1841
+ if token or api_key:
1842
+ interceptors.append(AuthInterceptor(token=token, api_key=api_key, api_key_header=api_key_header))
1843
+
1844
+ # User agent interceptor
1845
+ if user_agent:
1846
+ interceptors.append(UserAgentInterceptor(user_agent))
1847
+
1848
+ # Logging interceptor
1849
+ if enable_logging:
1850
+ interceptors.append(LoggingInterceptor())
1851
+
1852
+ # Custom interceptors
1853
+ if custom_interceptors:
1854
+ interceptors.extend(custom_interceptors)
1855
+
1856
+ # Initialize dispatcher and receiver
1857
+ self.dispatcher = Dispatcher(interceptors)
1858
+ self.receiver = Receiver(interceptors)
1859
+
1860
+ # Initialize API clients
1861
+ ${apiProperties}
1862
+
1863
+ async def __aenter__(self):
1864
+ return self
1865
+
1866
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
1867
+ await self.close()
1868
+
1869
+ async def close(self):
1870
+ """Close the HTTP client."""
1871
+ await self.dispatcher.close()
1872
+ `;
1873
+ await settings.writer(output, {
1874
+ ...models,
1875
+ ...apiClasses,
1876
+ "client.py": clientCode,
1877
+ "http/dispatcher.py": dispatcher_default,
1878
+ "http/interceptors.py": interceptors_default,
1879
+ "http/responses.py": responses_default,
1880
+ "__init__.py": `"""SDK package."""
1881
+
1882
+ from .client import ${clientName}
1883
+
1884
+ __all__ = ['${clientName}']
1885
+ `
1886
+ });
1887
+ if (settings.mode === "full") {
1888
+ const requirements = `# HTTP client
1889
+ httpx>=0.24.0,<1.0.0
1890
+
1891
+ # Data validation and serialization
1892
+ pydantic>=2.0.0,<3.0.0
1893
+
1894
+ # Enhanced type hints
1895
+ typing-extensions>=4.0.0
1896
+
1897
+ # Optional: For better datetime handling
1898
+ python-dateutil>=2.8.0
1899
+ `;
1900
+ await settings.writer(output, {
1901
+ "requirements.txt": requirements
1902
+ });
1903
+ }
1904
+ const metadata = await readWriteMetadata(
1905
+ settings.output,
1906
+ Array.from(writtenFiles)
1907
+ );
1908
+ if (settings.cleanup !== false && writtenFiles.size > 0) {
1909
+ await cleanFiles(metadata.content, settings.output, [
1910
+ "/__init__.py",
1911
+ "requirements.txt",
1912
+ "/metadata.json"
1913
+ ]);
1914
+ }
1915
+ await settings.writer(output, {
1916
+ "models/__init__.py": await generateModuleInit(
1917
+ join(output, "models"),
1918
+ settings.readFolder
1919
+ ),
1920
+ "inputs/__init__.py": await generateModuleInit(
1921
+ join(output, "inputs"),
1922
+ settings.readFolder
1923
+ ),
1924
+ "outputs/__init__.py": await generateModuleInit(
1925
+ join(output, "outputs"),
1926
+ settings.readFolder
1927
+ ),
1928
+ "api/__init__.py": await generateModuleInit(
1929
+ join(output, "api"),
1930
+ settings.readFolder
1931
+ ),
1932
+ "http/__init__.py": `"""HTTP utilities."""
1933
+
1934
+ from .dispatcher import Dispatcher, RequestConfig
1935
+ from .interceptors import *
1936
+ from .responses import *
1937
+
1938
+ __all__ = [
1939
+ 'Dispatcher',
1940
+ 'RequestConfig',
1941
+ 'ApiResponse',
1942
+ 'ErrorResponse',
1943
+ 'Interceptor',
1944
+ 'BaseUrlInterceptor',
1945
+ 'LoggingInterceptor',
1946
+ 'AuthInterceptor',
1947
+ ]
1948
+ `
1949
+ });
1950
+ if (settings.formatCode) {
1951
+ await settings.formatCode({ output: settings.output });
1952
+ }
1953
+ }
1954
+ async function generateModuleInit(folder, readFolder) {
1955
+ try {
1956
+ const files = await readFolder(folder);
1957
+ const pyFiles = files.filter(
1958
+ (file) => file.fileName.endsWith(".py") && file.fileName !== "__init__.py"
1959
+ ).map((file) => file.fileName.replace(".py", ""));
1960
+ if (pyFiles.length === 0) {
1961
+ return '"""Package module."""\n';
1962
+ }
1963
+ const imports = pyFiles.map((name) => `from .${name} import *`).join("\n");
1964
+ return `"""Package module."""
1965
+
1966
+ ${imports}
1967
+ `;
1968
+ } catch {
1969
+ return '"""Package module."""\\n';
1970
+ }
1971
+ }
1972
+ function toInputs(spec, { entry, operation }) {
1973
+ const inputName = entry.inputName || "Input";
1974
+ const haveInput = !isEmpty(operation.parameters) || !isEmpty(operation.requestBody);
1975
+ let contentType = "json";
1976
+ if (operation.requestBody && !isRef2(operation.requestBody)) {
1977
+ const content = operation.requestBody.content;
1978
+ if (content) {
1979
+ const contentTypes = Object.keys(content);
1980
+ if (contentTypes.some((type) => type.includes("multipart"))) {
1981
+ contentType = "multipart";
1982
+ } else if (contentTypes.some((type) => type.includes("form"))) {
1983
+ contentType = "form";
1984
+ }
1985
+ }
1986
+ }
1987
+ return {
1988
+ inputName,
1989
+ haveInput,
1990
+ contentType
1991
+ };
1992
+ }
1993
+ function toOutput(spec, operation) {
1994
+ if (!operation.responses) {
1995
+ return null;
1996
+ }
1997
+ const successResponse = Object.entries(operation.responses).find(
1998
+ ([code]) => isSuccessStatusCode(Number(code))
1999
+ );
2000
+ if (!successResponse) {
2001
+ return null;
2002
+ }
2003
+ const [statusCode, response] = successResponse;
2004
+ if (isRef2(response)) {
2005
+ return null;
2006
+ }
2007
+ const content = response.content;
2008
+ if (!content) {
2009
+ return { returnType: "None", successModel: null, errorModel: null };
2010
+ }
2011
+ const jsonContent = Object.entries(content).find(
2012
+ ([type]) => parseJsonContentType(type)
2013
+ );
2014
+ if (!jsonContent) {
2015
+ return {
2016
+ returnType: "httpx.Response",
2017
+ successModel: null,
2018
+ errorModel: null
2019
+ };
2020
+ }
2021
+ const [, mediaType] = jsonContent;
2022
+ const schema = mediaType.schema;
2023
+ if (!schema || isRef2(schema)) {
2024
+ return { returnType: "Any", successModel: null, errorModel: null };
2025
+ }
2026
+ const emitter = new PythonEmitter(spec);
2027
+ const result = emitter.handle(schema, {});
2028
+ return {
2029
+ returnType: result.type || "Any",
2030
+ successModel: result.type,
2031
+ errorModel: null
2032
+ // TODO: Handle error models
2033
+ };
2034
+ }
2035
+ async function serializeModels(spec, emitter) {
2036
+ const models = {};
2037
+ const standardImports = [
2038
+ "from typing import Any, Dict, List, Optional, Union, Literal",
2039
+ "from pydantic import BaseModel, Field",
2040
+ "from datetime import datetime, date",
2041
+ "from uuid import UUID",
2042
+ "from enum import Enum"
2043
+ ].join("\n");
2044
+ emitter.onEmit((name, content, schema) => {
2045
+ const fullContent = `${standardImports}
2046
+ ${schema["x-inputname"] ? "from ..http.dispatcher import RequestConfig" : ""}
2047
+
2048
+
2049
+ ${content}`;
2050
+ if (schema["x-inputname"]) {
2051
+ models[`inputs/${snakecase2(name)}.py`] = fullContent;
2052
+ } else if (schema["x-response-name"]) {
2053
+ models[`outputs/${snakecase2(name)}.py`] = fullContent;
2054
+ } else {
2055
+ models[`models/${snakecase2(name)}.py`] = fullContent;
2056
+ }
2057
+ });
2058
+ if (spec.components?.schemas) {
2059
+ for (const [name, schema] of Object.entries(spec.components.schemas)) {
2060
+ if (!isRef2(schema)) {
2061
+ emitter.handle(schema, { name });
2062
+ }
2063
+ }
2064
+ }
2065
+ return models;
2066
+ }
2067
+
2068
+ // packages/cli/src/lib/langs/python.ts
2069
+ import { augmentSpec as augmentSpec2, loadSpec as loadSpec2 } from "@sdk-it/spec";
2070
+ var python_default = new Command3("python").description("Generate Python SDK").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(true)).option("-l, --language <language>", "Programming language for the SDK").option("-n, --name <n>", "Name of the generated client", "Client").option("-v, --verbose", "Verbose output", false).option("--formatter <formatter>", "Formatter to use for the generated code").action(async (options) => {
2071
+ const spec = augmentSpec2({ spec: await loadSpec2(options.spec) }, true);
2072
+ await generate2(spec, {
2073
+ output: options.output,
2074
+ mode: options.mode || "full",
2075
+ name: options.name,
2076
+ formatCode: ({ output }) => {
2077
+ if (options.formatter) {
2078
+ const [command2, ...args] = options.formatter.split(" ");
2079
+ execFile2(command2, args, {
2080
+ env: { ...process.env, SDK_IT_OUTPUT: output }
2081
+ });
2082
+ } else {
2083
+ try {
2084
+ execSync2(`black ${shellEnv("SDK_IT_OUTPUT")}`, {
2085
+ env: { ...process.env, SDK_IT_OUTPUT: output },
2086
+ stdio: options.verbose ? "inherit" : "pipe"
2087
+ });
2088
+ } catch {
2089
+ try {
2090
+ execSync2(`ruff format ${shellEnv("SDK_IT_OUTPUT")}`, {
2091
+ env: { ...process.env, SDK_IT_OUTPUT: output },
2092
+ stdio: options.verbose ? "inherit" : "pipe"
2093
+ });
2094
+ } catch {
2095
+ if (options.verbose) {
2096
+ console.warn(
2097
+ "No Python formatter found (black or ruff). Skipping formatting."
2098
+ );
2099
+ }
2100
+ }
2101
+ }
2102
+ }
2103
+ }
2104
+ });
2105
+ });
2106
+
49
2107
  // packages/cli/src/lib/langs/typescript.ts
50
- import { Command as Command2, Option as Option2 } from "commander";
2108
+ import { Command as Command4, Option as Option2 } from "commander";
51
2109
  import { publish } from "libnpmpublish";
52
- import { execFile as execFile2, execSync as execSync2, spawnSync } from "node:child_process";
2110
+ import { execFile as execFile3, execSync as execSync3, spawnSync } from "node:child_process";
53
2111
  import { readFile } from "node:fs/promises";
54
2112
  import { tmpdir } from "node:os";
55
- import { join } from "node:path";
2113
+ import { join as join2 } from "node:path";
56
2114
  import getAuthToken from "registry-auth-token";
57
- import { writeFiles } from "@sdk-it/core/file-system.js";
58
- import { augmentSpec as augmentSpec2, loadSpec as loadSpec2 } from "@sdk-it/spec";
59
- import { generate as generate2 } from "@sdk-it/typescript";
60
- var command = new Command2("typescript").alias("ts").description("Generate TypeScript SDK").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(false)).option(
2115
+ import { writeFiles as writeFiles2 } from "@sdk-it/core/file-system.js";
2116
+ import { loadSpec as loadSpec3 } from "@sdk-it/spec";
2117
+ import { generate as generate3 } from "@sdk-it/typescript";
2118
+ var command = new Command4("typescript").alias("ts").description("Generate TypeScript SDK").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(false)).option(
61
2119
  "--useTsExtension [value]",
62
2120
  "Use .ts extension for generated files",
63
2121
  (value) => value === "false" ? false : true,
@@ -82,7 +2140,11 @@ var command = new Command2("typescript").alias("ts").description("Generate TypeS
82
2140
  "Treat errors as values instead of throwing them",
83
2141
  (value) => value === "false" ? false : true,
84
2142
  false
85
- ).option("--no-default-formatter", "Do not use the default formatter").option("--no-install", "Do not install dependencies").option("-v, --verbose", "Verbose output", false).addOption(
2143
+ ).option("--no-default-formatter", "Do not use the default formatter").option("--no-install", "Do not install dependencies").option("-v, --verbose", "Verbose output", false).option(
2144
+ "--pagination <pagination>",
2145
+ 'Configure pagination (e.g., "false", "true", "guess=false")',
2146
+ "true"
2147
+ ).addOption(
86
2148
  new Option2(
87
2149
  "--publish <publish>",
88
2150
  "Publish the SDK to a package registry (npm, github, or a custom registry)"
@@ -94,13 +2156,7 @@ var command = new Command2("typescript").alias("ts").description("Generate TypeS
94
2156
  });
95
2157
  return;
96
2158
  }
97
- const spec = augmentSpec2(
98
- {
99
- spec: await loadSpec2(options.spec),
100
- responses: { flattenErrorResponses: true }
101
- },
102
- false
103
- );
2159
+ const spec = await loadSpec3(options.spec);
104
2160
  if (options.output) {
105
2161
  await emitLocal(spec, { ...options, output: options.output });
106
2162
  }
@@ -112,11 +2168,12 @@ var command = new Command2("typescript").alias("ts").description("Generate TypeS
112
2168
  }
113
2169
  });
114
2170
  async function emitLocal(spec, options) {
115
- await generate2(spec, {
116
- writer: writeFiles,
2171
+ await generate3(spec, {
2172
+ writer: writeFiles2,
117
2173
  output: options.output,
118
2174
  mode: options.mode || "minimal",
119
2175
  name: options.name,
2176
+ pagination: parsePagination(parseDotConfig(options.pagination ?? "true")),
120
2177
  style: {
121
2178
  name: "github",
122
2179
  outputType: options.outputType,
@@ -127,7 +2184,7 @@ async function emitLocal(spec, options) {
127
2184
  formatCode: ({ env, output }) => {
128
2185
  if (options.formatter) {
129
2186
  const [command2, ...args] = options.formatter.split(" ");
130
- execFile2(command2, args, {
2187
+ execFile3(command2, args, {
131
2188
  env: { ...env, SDK_IT_OUTPUT: output }
132
2189
  });
133
2190
  } else if (options.defaultFormatter) {
@@ -144,7 +2201,7 @@ async function emitLocal(spec, options) {
144
2201
  });
145
2202
  if (options.install && options.mode === "full") {
146
2203
  console.log("Installing dependencies...");
147
- execSync2("npm install", {
2204
+ execSync3("npm install", {
148
2205
  cwd: options.output,
149
2206
  stdio: options.verbose ? "inherit" : "pipe"
150
2207
  });
@@ -153,7 +2210,7 @@ async function emitLocal(spec, options) {
153
2210
  async function emitRemote(spec, options) {
154
2211
  const registry = options.publish === "npm" ? "https://registry.npmjs.org/" : options.publish === "github" ? "https://npm.pkg.github.com/" : options.publish;
155
2212
  console.log("Publishing to registry:", registry);
156
- const path = join(tmpdir(), crypto.randomUUID());
2213
+ const path = join2(tmpdir(), crypto.randomUUID());
157
2214
  await emitLocal(spec, {
158
2215
  ...options,
159
2216
  output: path,
@@ -161,7 +2218,7 @@ async function emitRemote(spec, options) {
161
2218
  mode: "full"
162
2219
  });
163
2220
  const manifest = JSON.parse(
164
- await readFile(join(path, "package.json"), "utf-8")
2221
+ await readFile(join2(path, "package.json"), "utf-8")
165
2222
  );
166
2223
  const registryUrl = new URL(registry);
167
2224
  const npmrc = process.env.NPM_TOKEN ? {
@@ -176,9 +2233,9 @@ async function emitRemote(spec, options) {
176
2233
  "No npm auth token found in .npmrc or environment. please provide NPM_TOKEN."
177
2234
  );
178
2235
  }
179
- const packResult = execSync2("npm pack --pack-destination .", { cwd: path });
2236
+ const packResult = execSync3("npm pack --pack-destination .", { cwd: path });
180
2237
  const [tgzName] = packResult.toString().trim().split("\n");
181
- await publish(manifest, await readFile(join(path, tgzName)), {
2238
+ await publish(manifest, await readFile(join2(path, tgzName)), {
182
2239
  registry,
183
2240
  defaultTag: "latest",
184
2241
  forceAuth: {
@@ -191,9 +2248,9 @@ async function emitRemote(spec, options) {
191
2248
  var typescript_default = command;
192
2249
 
193
2250
  // packages/cli/src/lib/cli.ts
194
- var generate3 = new Command3("generate").addCommand(typescript_default).addCommand(dart_default);
195
- var cli = program.description(`CLI tool to interact with SDK-IT.`).addCommand(generate3, { isDefault: true }).addCommand(
196
- new Command3("_internal").action(() => {
2251
+ var generate4 = new Command5("generate").addCommand(typescript_default).addCommand(python_default).addCommand(dart_default).addCommand(apiref_default);
2252
+ var cli = program.description(`CLI tool to interact with SDK-IT.`).addCommand(generate4, { isDefault: true }).addCommand(
2253
+ new Command5("_internal").action(() => {
197
2254
  }),
198
2255
  { hidden: true }
199
2256
  ).parse(process.argv);