@sdk-it/cli 0.44.0 → 0.46.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/bin.js ADDED
@@ -0,0 +1,3179 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/src/lib/cli.ts
4
+ import { Command as Command7, program } from "commander";
5
+ import { readJson as readJson2 } from "@sdk-it/core/file-system.js";
6
+
7
+ // packages/cli/src/lib/commands/init.ts
8
+ import { checkbox, confirm, input, select } from "@inquirer/prompts";
9
+ import { Command } from "commander";
10
+ import { writeFile as writeFile4 } from "node:fs/promises";
11
+ import { resolve as resolve6 } from "node:path";
12
+
13
+ // packages/cli/src/lib/project.ts
14
+ import { resolve as resolve3 } from "node:path";
15
+
16
+ // packages/cli/src/lib/project/analysis.ts
17
+ import { createRequire } from "node:module";
18
+ import ts from "typescript";
19
+ import { defaultTypesMap, getProgram } from "@sdk-it/core";
20
+ import { analyze } from "@sdk-it/generic";
21
+ import { responseAnalyzer as honoResponseAnalyzer } from "@sdk-it/hono";
22
+ async function analyzeProject(tsconfig, config) {
23
+ const framework = resolveFramework(tsconfig, config.framework);
24
+ if (framework === "auto") {
25
+ throw new Error(
26
+ `Could not detect a supported framework from ${config.tsconfig}. Set framework to 'hono' to select it explicitly.`
27
+ );
28
+ }
29
+ const prisma = config.preset === "none" ? void 0 : detectPrisma(tsconfig);
30
+ if (config.preset === "prisma" && !prisma) {
31
+ throw new Error(
32
+ `Prisma preset was requested, but no Prisma client import was found in ${tsconfig}. Run prisma generate or set preset to 'none'.`
33
+ );
34
+ }
35
+ const { paths, components } = await analyze(tsconfig, {
36
+ responseAnalyzer: honoResponseAnalyzer,
37
+ ...prisma ? {
38
+ imports: prisma.imports,
39
+ typesMap: {
40
+ ...defaultTypesMap,
41
+ Decimal: "string"
42
+ }
43
+ } : {}
44
+ });
45
+ return {
46
+ openapi: "3.1.0",
47
+ info: {
48
+ title: "API",
49
+ version: "0.0.0"
50
+ },
51
+ paths,
52
+ components
53
+ };
54
+ }
55
+ function resolveFramework(tsconfig, configured) {
56
+ return configured === void 0 || configured === "auto" ? detectFramework(tsconfig) : configured;
57
+ }
58
+ function detectFramework(tsconfig) {
59
+ const program2 = getProgram(tsconfig);
60
+ for (const sourceFile of program2.getSourceFiles()) {
61
+ if (sourceFile.isDeclarationFile) continue;
62
+ for (const statement of sourceFile.statements) {
63
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && (statement.moduleSpecifier.text === "hono" || statement.moduleSpecifier.text.startsWith("@sdk-it/hono"))) {
64
+ return "hono";
65
+ }
66
+ }
67
+ }
68
+ return "auto";
69
+ }
70
+ function detectPrisma(tsconfig) {
71
+ const program2 = getProgram(tsconfig);
72
+ const imports = [];
73
+ const reportedModules = /* @__PURE__ */ new Set();
74
+ for (const sourceFile of program2.getSourceFiles()) {
75
+ if (sourceFile.isDeclarationFile) continue;
76
+ for (const statement of sourceFile.statements) {
77
+ const prismaImport = getPrismaImport(statement);
78
+ if (!prismaImport) continue;
79
+ const resolvedModule = ts.resolveModuleName(
80
+ prismaImport.moduleSpecifier,
81
+ sourceFile.fileName,
82
+ program2.getCompilerOptions(),
83
+ ts.sys
84
+ );
85
+ if (!resolvedModule.resolvedModule) continue;
86
+ let runtimeModule;
87
+ try {
88
+ runtimeModule = createRequire(sourceFile.fileName).resolve(
89
+ prismaImport.moduleSpecifier
90
+ );
91
+ } catch {
92
+ continue;
93
+ }
94
+ if (!reportedModules.has(runtimeModule)) {
95
+ console.log(`SDKIT: detected Prisma from ${runtimeModule}`);
96
+ reportedModules.add(runtimeModule);
97
+ }
98
+ for (const { imported, local } of prismaImport.bindings) {
99
+ if (!imports.some(
100
+ (item) => item.import === local && item.from === runtimeModule
101
+ )) {
102
+ imports.push({
103
+ import: local,
104
+ from: runtimeModule,
105
+ property: imported
106
+ });
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return imports.length > 0 ? { imports } : void 0;
112
+ }
113
+ function getPrismaImport(statement) {
114
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.importClause?.namedBindings || !ts.isNamedImports(statement.importClause.namedBindings)) {
115
+ return void 0;
116
+ }
117
+ const bindings = statement.importClause.namedBindings.elements.map((element) => ({
118
+ imported: element.propertyName?.text ?? element.name.text,
119
+ local: element.name.text
120
+ })).filter(({ imported }) => imported === "Prisma" || imported === "$Enums");
121
+ return bindings.length > 0 ? { moduleSpecifier: statement.moduleSpecifier.text, bindings } : void 0;
122
+ }
123
+
124
+ // packages/cli/src/lib/project/output.ts
125
+ import { writeFile as writeFile2 } from "node:fs/promises";
126
+ import { join as join3, resolve } from "node:path";
127
+ import { generate } from "@sdk-it/typescript";
128
+
129
+ // packages/cli/src/lib/project/cache.ts
130
+ import { createHash } from "node:crypto";
131
+ import { access, readFile, readdir } from "node:fs/promises";
132
+ import { createRequire as createRequire2 } from "node:module";
133
+ import { join, relative } from "node:path";
134
+ import ts2 from "typescript";
135
+ var require2 = createRequire2(import.meta.url);
136
+ var projectGeneratorVersions = {
137
+ cli: require2("@sdk-it/cli/package.json").version,
138
+ compiler: ts2.version,
139
+ typescript: require2("@sdk-it/typescript/package.json").version
140
+ };
141
+ function hashProject(openapi, packageName) {
142
+ return createHash("sha256").update(JSON.stringify({ openapi, packageName, projectGeneratorVersions })).digest("hex");
143
+ }
144
+ async function isCurrentGeneratedPackage(output, hash) {
145
+ return await readOptionalFile(join(output, ".project-hash")) === hash && await generatedPackageExists(output);
146
+ }
147
+ async function generatedPackageExists(output) {
148
+ try {
149
+ const sourceRoot = join(output, "src");
150
+ const sources = await findSourceFiles(sourceRoot);
151
+ if (!sources.includes(join(sourceRoot, "index.ts"))) return false;
152
+ await Promise.all([
153
+ access(join(output, "package.json")),
154
+ ...sources.flatMap(
155
+ (source) => expectedCompiledFiles(output, sourceRoot, source).map(
156
+ (file) => access(file)
157
+ )
158
+ )
159
+ ]);
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+ function expectedCompiledFiles(output, sourceRoot, source) {
166
+ const compiled = relative(sourceRoot, source).slice(0, -3);
167
+ return [
168
+ join(output, "dist", `${compiled}.js`),
169
+ join(output, "dist", `${compiled}.d.ts`)
170
+ ];
171
+ }
172
+ async function findSourceFiles(directory) {
173
+ const entries = await readdir(directory, { withFileTypes: true });
174
+ const files = await Promise.all(
175
+ entries.map(async (entry) => {
176
+ const path = join(directory, entry.name);
177
+ if (entry.isDirectory()) return findSourceFiles(path);
178
+ return entry.isFile() && path.endsWith(".ts") && !path.endsWith(".d.ts") ? [path] : [];
179
+ })
180
+ );
181
+ return files.flat();
182
+ }
183
+ async function readOptionalFile(path) {
184
+ try {
185
+ return await readFile(path, "utf8");
186
+ } catch (error) {
187
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
188
+ return void 0;
189
+ }
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ // packages/cli/src/lib/project/compiler.ts
195
+ import { readFile as readFile2, writeFile } from "node:fs/promises";
196
+ import { join as join2 } from "node:path";
197
+ import ts3 from "typescript";
198
+ async function compileGeneratedPackage(output, packageName) {
199
+ const source = join2(output, "src");
200
+ const program2 = ts3.createProgram({
201
+ rootNames: ts3.sys.readDirectory(source, [".ts"]),
202
+ options: {
203
+ allowSyntheticDefaultImports: true,
204
+ declaration: true,
205
+ module: ts3.ModuleKind.ESNext,
206
+ moduleResolution: ts3.ModuleResolutionKind.Bundler,
207
+ noEmitOnError: true,
208
+ outDir: join2(output, "dist"),
209
+ rewriteRelativeImportExtensions: true,
210
+ rootDir: source,
211
+ skipLibCheck: true,
212
+ target: ts3.ScriptTarget.ESNext,
213
+ verbatimModuleSyntax: true
214
+ }
215
+ });
216
+ const result = program2.emit();
217
+ const diagnostics = [
218
+ ...ts3.getPreEmitDiagnostics(program2),
219
+ ...result.diagnostics
220
+ ].filter((diagnostic) => diagnostic.category === ts3.DiagnosticCategory.Error);
221
+ if (result.emitSkipped || diagnostics.length > 0) {
222
+ throw new Error(formatCompilationError(output, diagnostics));
223
+ }
224
+ await synchronizeGeneratedManifest(output, packageName);
225
+ }
226
+ function formatCompilationError(output, diagnostics) {
227
+ return `Failed to compile generated client:
228
+ ${ts3.formatDiagnosticsWithColorAndContext(
229
+ diagnostics,
230
+ {
231
+ getCanonicalFileName: (fileName) => fileName,
232
+ getCurrentDirectory: () => output,
233
+ getNewLine: () => "\n"
234
+ }
235
+ )}`;
236
+ }
237
+ async function synchronizeGeneratedManifest(output, packageName) {
238
+ const manifestPath = join2(output, "package.json");
239
+ const manifest = JSON.parse(
240
+ await readFile2(manifestPath, "utf8")
241
+ );
242
+ Object.assign(manifest, {
243
+ name: packageName,
244
+ version: "0.0.1",
245
+ type: "module",
246
+ main: "./dist/index.js",
247
+ module: "./dist/index.js",
248
+ types: "./dist/index.d.ts"
249
+ });
250
+ manifest.publishConfig = { ...manifest.publishConfig, access: "public" };
251
+ manifest.exports = {
252
+ ...manifest.exports,
253
+ "./package.json": "./package.json",
254
+ ".": {
255
+ types: "./dist/index.d.ts",
256
+ import: "./dist/index.js",
257
+ default: "./dist/index.js"
258
+ }
259
+ };
260
+ manifest.dependencies = {
261
+ ...manifest.dependencies,
262
+ "fast-content-type-parse": "^3.0.0",
263
+ zod: "^4.3.0"
264
+ };
265
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
266
+ `);
267
+ }
268
+
269
+ // packages/cli/src/lib/project/output.ts
270
+ async function writeProjectClient(openapi, config) {
271
+ const output = resolve(config.output ?? ".sdk-it");
272
+ const packageName = config.packageName ?? "@sdk-it/client";
273
+ const hash = hashProject(openapi, packageName);
274
+ if (await isCurrentGeneratedPackage(output, hash)) return;
275
+ await generate(openapi, {
276
+ output,
277
+ mode: "full",
278
+ name: "Client",
279
+ packageName,
280
+ readme: false
281
+ });
282
+ await compileGeneratedPackage(output, packageName);
283
+ await writeFile2(join3(output, ".project-hash"), hash);
284
+ }
285
+
286
+ // packages/cli/src/lib/project/config.ts
287
+ import { access as access2, readFile as readFile3, stat, writeFile as writeFile3 } from "node:fs/promises";
288
+ import { dirname, join as join4, relative as relative2, resolve as resolve2 } from "node:path";
289
+ import { pathToFileURL } from "node:url";
290
+ async function loadProjectConfig(options = {}) {
291
+ const cwd = resolve2(options.cwd ?? process.cwd());
292
+ const configPath = options.config ? resolve2(cwd, options.config) : await findProjectConfig(cwd);
293
+ const loaded = await import(pathToFileURL(configPath).href);
294
+ const config = loaded.default;
295
+ if (!config || typeof config.tsconfig !== "string") {
296
+ throw new Error(
297
+ `Expected ${configPath} to default export an SDK-IT config with a tsconfig path.`
298
+ );
299
+ }
300
+ const directory = dirname(configPath);
301
+ return {
302
+ ...config,
303
+ tsconfig: resolve2(directory, config.tsconfig),
304
+ output: resolve2(directory, config.output ?? ".sdk-it")
305
+ };
306
+ }
307
+ async function initializeProject(options) {
308
+ const cwd = resolve2(options.cwd ?? process.cwd());
309
+ const configPath = join4(cwd, "sdk-it.config.ts");
310
+ const tsconfigPath = resolve2(cwd, options.tsconfig);
311
+ await validateTsconfig(tsconfigPath);
312
+ const tsconfig = relative2(cwd, tsconfigPath).replaceAll("\\", "/");
313
+ const relativeTsconfig = tsconfig.startsWith(".") ? tsconfig : `./${tsconfig}`;
314
+ const configSource = `import { defineConfig } from '@sdk-it/cli';
315
+
316
+ export default defineConfig({
317
+ tsconfig: '${relativeTsconfig}',
318
+ });
319
+ `;
320
+ const existingConfig = await readOptionalFile2(configPath);
321
+ if (existingConfig !== void 0 && existingConfig !== configSource) {
322
+ throw new Error(
323
+ `${configPath} already exists with different settings. Review it before replacing the file.`
324
+ );
325
+ }
326
+ const packagePath = join4(cwd, "package.json");
327
+ const manifest = JSON.parse(
328
+ await readFile3(packagePath, "utf8")
329
+ );
330
+ const manifestChanged = addGeneratedWorkspace(manifest);
331
+ const gitignorePath = join4(cwd, ".gitignore");
332
+ const gitignore = await readOptionalFile2(gitignorePath) ?? "";
333
+ if (!ignoresGeneratedWorkspace(gitignore)) {
334
+ const prefix = gitignore.length > 0 && !gitignore.endsWith("\n") ? "\n" : "";
335
+ await writeFile3(gitignorePath, `${gitignore}${prefix}.sdk-it/
336
+ `);
337
+ }
338
+ if (manifestChanged) {
339
+ await writeFile3(packagePath, `${JSON.stringify(manifest, null, 2)}
340
+ `);
341
+ }
342
+ if (existingConfig === void 0) {
343
+ await writeFile3(configPath, configSource);
344
+ }
345
+ }
346
+ function addGeneratedWorkspace(manifest) {
347
+ const workspaces = manifest.workspaces;
348
+ if (Array.isArray(workspaces)) {
349
+ if (workspaces.includes(".sdk-it")) return false;
350
+ workspaces.push(".sdk-it");
351
+ return true;
352
+ }
353
+ if (workspaces && Array.isArray(workspaces.packages)) {
354
+ if (workspaces.packages.includes(".sdk-it")) return false;
355
+ workspaces.packages.push(".sdk-it");
356
+ return true;
357
+ }
358
+ manifest.workspaces = [".sdk-it"];
359
+ return true;
360
+ }
361
+ function ignoresGeneratedWorkspace(gitignore) {
362
+ return gitignore.split(/\r?\n/).some((line) => line.trim() === ".sdk-it/" || line.trim() === ".sdk-it");
363
+ }
364
+ async function validateTsconfig(path) {
365
+ try {
366
+ if ((await stat(path)).isFile()) return;
367
+ } catch (error) {
368
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
369
+ throw error;
370
+ }
371
+ }
372
+ throw new Error(`Could not find a TypeScript project at ${path}.`);
373
+ }
374
+ async function findProjectConfig(start) {
375
+ let directory = start;
376
+ while (true) {
377
+ const candidate = join4(directory, "sdk-it.config.ts");
378
+ try {
379
+ await access2(candidate);
380
+ return candidate;
381
+ } catch {
382
+ const parent = dirname(directory);
383
+ if (parent === directory) {
384
+ throw new Error(
385
+ `Could not find sdk-it.config.ts from ${start} or any parent directory.`
386
+ );
387
+ }
388
+ directory = parent;
389
+ }
390
+ }
391
+ }
392
+ async function readOptionalFile2(path) {
393
+ try {
394
+ return await readFile3(path, "utf8");
395
+ } catch (error) {
396
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
397
+ return void 0;
398
+ }
399
+ throw error;
400
+ }
401
+ }
402
+
403
+ // packages/cli/src/lib/project.ts
404
+ async function generateProject(config) {
405
+ const tsconfig = resolve3(config.tsconfig);
406
+ const openapi = await analyzeProject(tsconfig, config);
407
+ await writeProjectClient(openapi, config);
408
+ }
409
+
410
+ // packages/cli/src/lib/commands/find-framework.ts
411
+ import { resolve as resolve4 } from "node:path";
412
+ import { exist } from "@sdk-it/core/file-system.js";
413
+ var monorepoIndicators = {
414
+ lerna: () => exist(resolve4(process.cwd(), "lerna.json")),
415
+ nx: () => exist(resolve4(process.cwd(), "nx.json")),
416
+ pnpm: () => exist(resolve4(process.cwd(), "pnpm-workspace.yaml")),
417
+ rush: () => exist(resolve4(process.cwd(), "rush.json"))
418
+ };
419
+ async function detectMonorepo() {
420
+ for (const [indicator, check] of Object.entries(monorepoIndicators)) {
421
+ if (await check()) {
422
+ return indicator;
423
+ }
424
+ }
425
+ return void 0;
426
+ }
427
+
428
+ // packages/cli/src/lib/commands/find-spec-file.ts
429
+ import { resolve as resolve5 } from "node:path";
430
+ import { exist as exist2 } from "@sdk-it/core/file-system.js";
431
+ async function findSpecFile() {
432
+ const commonNames = [
433
+ "openapi.json",
434
+ "openapi.yaml",
435
+ "openapi.yml",
436
+ "swagger.json",
437
+ "swagger.yaml",
438
+ "swagger.yml",
439
+ "api.json",
440
+ "api.yaml",
441
+ "api.yml",
442
+ "spec.json",
443
+ "spec.yaml",
444
+ "spec.yml",
445
+ "schema.json",
446
+ "schema.yaml",
447
+ "schema.yml"
448
+ ];
449
+ for (const name of commonNames) {
450
+ if (await exist2(resolve5(process.cwd(), name))) {
451
+ return `./${name}`;
452
+ }
453
+ }
454
+ return void 0;
455
+ }
456
+
457
+ // packages/cli/src/lib/commands/guess-default-package-name.ts
458
+ import { join as join5 } from "node:path";
459
+ import { readJson } from "@sdk-it/core/file-system.js";
460
+ async function guessTypescriptPackageName(consideringMultipleGenerator) {
461
+ try {
462
+ const packageJson = await readJson(
463
+ join5(process.cwd(), "package.json")
464
+ );
465
+ if (packageJson.name) {
466
+ const match = packageJson.name.match(/^@([^/]+)/);
467
+ if (match) {
468
+ const scope = match[1];
469
+ return consideringMultipleGenerator ? `@${scope}/ts-sdk` : `@${scope}/sdk`;
470
+ }
471
+ }
472
+ } catch {
473
+ }
474
+ return consideringMultipleGenerator ? "ts-sdk" : "sdk";
475
+ }
476
+
477
+ // packages/cli/src/lib/commands/init.ts
478
+ var specInput = async (defaultValue) => {
479
+ return input({
480
+ message: "OpenAPI or Postman specification file path:",
481
+ default: defaultValue || "./openapi.json"
482
+ });
483
+ };
484
+ var generatorConfigs = {
485
+ typescript: {
486
+ name: async (isMultipleGenerators = false) => {
487
+ const defaultName = await guessTypescriptPackageName(isMultipleGenerators);
488
+ return input({
489
+ message: "SDK package name:",
490
+ default: defaultName
491
+ });
492
+ },
493
+ spec: specInput,
494
+ output: async () => {
495
+ let defaultValue = "./ts-sdk";
496
+ const monorepo = await detectMonorepo();
497
+ if (monorepo === "nx") {
498
+ defaultValue = "./packages/ts-sdk";
499
+ }
500
+ return await input({
501
+ message: "Output directory:",
502
+ default: defaultValue
503
+ });
504
+ },
505
+ mode: async () => {
506
+ const options = {
507
+ mode: "full",
508
+ install: false
509
+ };
510
+ options.mode = await select({
511
+ message: "Generation mode:",
512
+ choices: [
513
+ {
514
+ name: "Full (generates package.json and tsconfig.json)",
515
+ value: "full"
516
+ },
517
+ {
518
+ name: "Minimal (generates only the client TypeScript files)",
519
+ value: "minimal"
520
+ }
521
+ ],
522
+ default: options.mode
523
+ });
524
+ if (options.mode === "full") {
525
+ const installDeps = await confirm({
526
+ message: "Install dependencies automatically?",
527
+ default: true
528
+ });
529
+ options.install = installDeps;
530
+ }
531
+ return options;
532
+ },
533
+ pagination: async () => {
534
+ let pagination = {
535
+ guess: false
536
+ };
537
+ const result = await confirm({
538
+ message: "Enable pagination support?",
539
+ default: false
540
+ });
541
+ if (result) {
542
+ pagination.guess = await confirm({
543
+ message: "Would you like to guess pagination parameters?",
544
+ default: false
545
+ });
546
+ } else {
547
+ pagination = false;
548
+ }
549
+ return pagination;
550
+ },
551
+ readme: () => confirm({
552
+ message: "Generate README file?",
553
+ default: true
554
+ }),
555
+ defaultFormatter: () => confirm({
556
+ message: "Use default formatter (prettier)?",
557
+ default: true
558
+ }),
559
+ framework: () => input({
560
+ message: "Framework integrating with the SDK (optional):"
561
+ }),
562
+ formatter: () => input({
563
+ message: 'Custom formatter command (optional, e.g., "prettier $SDK_IT_OUTPUT --write"):'
564
+ })
565
+ },
566
+ python: {
567
+ name: () => input({
568
+ message: "SDK package name:",
569
+ default: "my-python-sdk"
570
+ }),
571
+ spec: specInput,
572
+ output: () => input({
573
+ message: "Output directory:",
574
+ default: "./python-sdk"
575
+ }),
576
+ mode: async () => {
577
+ const isMonorepo = await detectMonorepo();
578
+ return select({
579
+ message: "Generation mode:",
580
+ choices: [
581
+ {
582
+ name: "Full (generates complete project structure)",
583
+ value: "full"
584
+ },
585
+ {
586
+ name: "Minimal (generates only the client files)",
587
+ value: "minimal"
588
+ }
589
+ ],
590
+ default: isMonorepo ? "full" : "full"
591
+ // Default to full, especially for monorepos
592
+ }).then((value) => value);
593
+ },
594
+ formatter: () => input({
595
+ message: 'Custom formatter command (optional, e.g., "black $SDK_IT_OUTPUT" or "ruff format $SDK_IT_OUTPUT"):'
596
+ })
597
+ },
598
+ dart: {
599
+ name: () => input({
600
+ message: "SDK package name:",
601
+ default: "my-dart-sdk"
602
+ }),
603
+ spec: specInput,
604
+ output: () => input({
605
+ message: "Output directory:",
606
+ default: "./dart-sdk"
607
+ }),
608
+ mode: async () => {
609
+ const isMonorepo = await detectMonorepo();
610
+ return select({
611
+ message: "Generation mode:",
612
+ choices: [
613
+ {
614
+ name: "Full (generates complete project structure)",
615
+ value: "full"
616
+ },
617
+ {
618
+ name: "Minimal (generates only the client files)",
619
+ value: "minimal"
620
+ }
621
+ ],
622
+ default: isMonorepo ? "full" : "full"
623
+ // Default to full, especially for monorepos
624
+ }).then((value) => value);
625
+ },
626
+ pagination: async () => {
627
+ let pagination = {
628
+ guess: false
629
+ };
630
+ const result = await confirm({
631
+ message: "Enable pagination support?",
632
+ default: false
633
+ });
634
+ if (result) {
635
+ pagination.guess = await confirm({
636
+ message: "Would you like to guess pagination parameters?",
637
+ default: false
638
+ });
639
+ } else {
640
+ pagination = false;
641
+ }
642
+ return pagination;
643
+ }
644
+ }
645
+ };
646
+ var init = new Command("init").description("Initialize SDK-IT configuration interactively").option("--project <tsconfig>", "Initialize from a backend tsconfig").action(async (options) => {
647
+ if (options.project) {
648
+ await initializeProject({ tsconfig: options.project });
649
+ console.log("SDK-IT project configuration initialized.");
650
+ return;
651
+ }
652
+ console.log("Welcome to SDK-IT! Let's set up your configuration.\n");
653
+ const possibleSpecFile = await findSpecFile();
654
+ const monorepo = await detectMonorepo();
655
+ if (possibleSpecFile) {
656
+ console.log(`\u{1F50D} Auto-detected API specification: ${possibleSpecFile}`);
657
+ }
658
+ if (monorepo) {
659
+ console.log(`\u{1F4E6} Detected monorepo setup`);
660
+ }
661
+ if (possibleSpecFile || monorepo) {
662
+ console.log("");
663
+ }
664
+ const config = {
665
+ generators: {}
666
+ };
667
+ const generators = await checkbox({
668
+ message: "Which SDK generators would you like to configure?",
669
+ loop: false,
670
+ instructions: false,
671
+ required: true,
672
+ choices: [
673
+ { name: "TypeScript", value: "typescript" },
674
+ { name: "Python", value: "python" },
675
+ { name: "Dart", value: "dart" }
676
+ ]
677
+ });
678
+ for (const generator of generators) {
679
+ console.log(`
680
+ Configuring ${generator} generator:`);
681
+ if (generator === "typescript") {
682
+ const tsConfig = generatorConfigs.typescript;
683
+ const isMultipleGenerators = generators.length > 1;
684
+ const generatorConfig = {
685
+ spec: await tsConfig.spec(possibleSpecFile),
686
+ output: await tsConfig.output(),
687
+ name: await tsConfig.name(isMultipleGenerators),
688
+ defaultFormatter: await tsConfig.defaultFormatter(),
689
+ readme: await tsConfig.readme(),
690
+ pagination: await tsConfig.pagination(),
691
+ ...await tsConfig.mode()
692
+ };
693
+ const customFramework = await tsConfig.framework();
694
+ if (customFramework) {
695
+ generatorConfig.framework = customFramework;
696
+ }
697
+ const customFormatter = await tsConfig.formatter();
698
+ if (customFormatter) {
699
+ generatorConfig.formatter = customFormatter;
700
+ }
701
+ config.generators.typescript = generatorConfig;
702
+ } else if (generator === "python") {
703
+ config.generators.python = {
704
+ spec: await generatorConfigs.python.spec(),
705
+ output: await generatorConfigs.python.output(),
706
+ mode: await generatorConfigs.python.mode(),
707
+ name: await generatorConfigs.python.name()
708
+ };
709
+ } else if (generator === "dart") {
710
+ config.generators.dart = {
711
+ spec: await generatorConfigs.dart.spec(),
712
+ output: await generatorConfigs.dart.output(),
713
+ mode: await generatorConfigs.dart.mode(),
714
+ name: await generatorConfigs.dart.name(),
715
+ pagination: await generatorConfigs.dart.pagination()
716
+ };
717
+ }
718
+ }
719
+ const generateReadme = await confirm({
720
+ message: "\nGenerate README documentation?",
721
+ default: true
722
+ });
723
+ if (generateReadme) {
724
+ const readmeSpec = await input({
725
+ message: "OpenAPI specification for README:",
726
+ default: config.generators.typescript?.spec || possibleSpecFile || "./openapi.yaml"
727
+ });
728
+ const readmeOutput = await input({
729
+ message: "README output file:",
730
+ default: "./README.md"
731
+ });
732
+ config.readme = {
733
+ spec: readmeSpec,
734
+ output: readmeOutput
735
+ };
736
+ }
737
+ const generateApiRef = await confirm({
738
+ message: "\nGenerate API reference documentation?",
739
+ default: false
740
+ });
741
+ if (generateApiRef) {
742
+ const autoDetected = await findSpecFile();
743
+ const apirefSpec = await input({
744
+ message: "OpenAPI specification for API reference:",
745
+ default: config.generators.typescript?.spec || autoDetected || "./openapi.yaml"
746
+ });
747
+ const apirefOutput = await input({
748
+ message: "API reference output directory:",
749
+ default: "./docs"
750
+ });
751
+ config.apiref = {
752
+ spec: apirefSpec,
753
+ output: apirefOutput
754
+ };
755
+ }
756
+ const configPath = resolve6(process.cwd(), "sdk-it.json");
757
+ await writeFile4(configPath, JSON.stringify(config, null, 2));
758
+ console.log(`
759
+ \u2705 Configuration saved to ${configPath}`);
760
+ console.log("\n\u{1F680} Next Steps:\n");
761
+ console.log("1. Generate your SDK(s):");
762
+ console.log(" npx @sdk-it/cli");
763
+ if (config.generators.typescript) {
764
+ console.log("2. Integrate TypeScript SDK:");
765
+ const importName = config.generators.typescript.name.replace(
766
+ /[^a-zA-Z0-9]/g,
767
+ ""
768
+ );
769
+ const outputDir = config.generators.typescript.output.replace("./", "");
770
+ console.log(` import { ${importName} } from './${outputDir}';`);
771
+ console.log(` const client = new ${importName}();`);
772
+ console.log(` const result = await client.request('GET /users');
773
+ `);
774
+ }
775
+ if (config.generators.python) {
776
+ console.log("2. Integrate Python SDK:");
777
+ const outputDir = config.generators.python.output.replace("./", "");
778
+ console.log(` # Add to your Python path or install locally`);
779
+ console.log(` from ${outputDir} import Client`);
780
+ console.log(` client = Client()`);
781
+ console.log(` result = client.users.list_users()
782
+ `);
783
+ }
784
+ if (config.generators.dart) {
785
+ console.log("2. Integrate Dart SDK:");
786
+ const outputDir = config.generators.dart.output.replace("./", "");
787
+ console.log(` # Add dependency to pubspec.yaml`);
788
+ console.log(` import 'package:${outputDir}/client.dart';`);
789
+ console.log(` final client = Client();`);
790
+ console.log(` final result = await client.users.listUsers();
791
+ `);
792
+ }
793
+ console.log("3. Check generated documentation:");
794
+ const outputs = [];
795
+ if (config.generators.typescript)
796
+ outputs.push(config.generators.typescript.output);
797
+ if (config.generators.python) outputs.push(config.generators.python.output);
798
+ if (config.generators.dart) outputs.push(config.generators.dart.output);
799
+ outputs.forEach((output) => {
800
+ if (output) {
801
+ console.log(
802
+ ` \u{1F4D6} ${output}/README.md - Usage examples and API reference`
803
+ );
804
+ }
805
+ });
806
+ if (config.readme) {
807
+ console.log(
808
+ ` \u{1F4D6} ${config.readme.output} - Generated API documentation`
809
+ );
810
+ }
811
+ if (config.apiref) {
812
+ console.log(` \u{1F310} ${config.apiref.output} - Interactive API reference`);
813
+ }
814
+ console.log("\n4. Useful commands:");
815
+ console.log(
816
+ " npx @sdk-it/cli # Regenerate SDKs after API changes"
817
+ );
818
+ console.log(
819
+ " npx @sdk-it/cli typescript --help # See TypeScript-specific options"
820
+ );
821
+ console.log(
822
+ " npx @sdk-it/cli python --help # See Python-specific options"
823
+ );
824
+ console.log(
825
+ " npx @sdk-it/cli dart --help # See Dart-specific options"
826
+ );
827
+ console.log("\n\u{1F4A1} Tips:");
828
+ console.log(
829
+ " \u2022 Update your API spec and re-run `npx @sdk-it/cli generate` to sync changes"
830
+ );
831
+ console.log(
832
+ " \u2022 Generated SDKs include TypeScript definitions for excellent IDE support"
833
+ );
834
+ console.log(
835
+ " \u2022 Check the README files for authentication and configuration options"
836
+ );
837
+ console.log("\n\u{1F4DA} Need help?");
838
+ console.log(" \u2022 Documentation: https://sdk-it.dev/docs");
839
+ console.log(
840
+ " \u2022 Examples: https://github.com/JanuaryLabs/sdk-it/tree/main/docs/examples"
841
+ );
842
+ console.log(" \u2022 Issues: https://github.com/JanuaryLabs/sdk-it/issues");
843
+ console.log("\nHappy coding! \u{1F389}\n");
844
+ });
845
+ var init_default = init;
846
+
847
+ // packages/cli/src/lib/generators/apiref.ts
848
+ import { Command as Command2 } from "commander";
849
+ import { execa } from "execa";
850
+ import { dirname as dirname2, join as join6 } from "node:path";
851
+
852
+ // packages/cli/src/lib/options.ts
853
+ import { Option } from "commander";
854
+ var specOption = new Option(
855
+ "-s, --spec <spec>",
856
+ "Path to OpenAPI specification file"
857
+ );
858
+ var outputOption = new Option(
859
+ "-o, --output <output>",
860
+ "Output directory for the generated SDK"
861
+ );
862
+ function shellEnv(name) {
863
+ return process.platform === "win32" ? `%${name}%` : `$${name}`;
864
+ }
865
+ function parseDotConfig(incoming) {
866
+ if (incoming === "false") {
867
+ return false;
868
+ }
869
+ if (incoming === "true") {
870
+ return true;
871
+ }
872
+ if (!incoming) {
873
+ return void 0;
874
+ }
875
+ const config = {};
876
+ const pairs = incoming.split(",");
877
+ for (const pair of pairs) {
878
+ if (pair.includes("=")) {
879
+ const [key, val] = pair.split("=", 2);
880
+ if (val === "true") {
881
+ config[key] = true;
882
+ continue;
883
+ }
884
+ if (val === "false") {
885
+ config[key] = false;
886
+ continue;
887
+ }
888
+ config[key] = val;
889
+ }
890
+ }
891
+ return config;
892
+ }
893
+ function parsePagination(config) {
894
+ if (config === true || config === void 0) {
895
+ return void 0;
896
+ }
897
+ if (config === false) {
898
+ return false;
899
+ }
900
+ return config;
901
+ }
902
+
903
+ // packages/cli/src/lib/generators/apiref.ts
904
+ var apiref_default = new Command2("apiref").description("Generate APIREF").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(true)).action(async (options) => {
905
+ await runApiRef(options.spec, options.output);
906
+ });
907
+ function runApiRef(spec, output) {
908
+ const packageDir = join6(dirname2(import.meta.url), "..", "..", "apiref");
909
+ return execa("nx", ["run", "apiref:build", "--verbose"], {
910
+ stdio: "inherit",
911
+ extendEnv: true,
912
+ cwd: packageDir,
913
+ env: {
914
+ VITE_SPEC: spec,
915
+ VITE_SDK_IT_OUTPUT: output
916
+ }
917
+ });
918
+ }
919
+
920
+ // packages/cli/src/lib/generators/dart.ts
921
+ import { Command as Command3 } from "commander";
922
+ import { execFile, execSync } from "node:child_process";
923
+ import { generate as generate2 } from "@sdk-it/dart";
924
+ import { loadSpec } from "@sdk-it/spec";
925
+ var dart_default = new Command3("dart").description("Generate Dart SDK").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(true)).option("-n, --name <name>", "Name of the generated client", "Client").option(
926
+ "--pagination <pagination>",
927
+ 'Configure pagination (e.g., "false", "true", "guess=false")',
928
+ "true"
929
+ ).option("-v, --verbose", "Verbose output", false).action(async (options) => {
930
+ await runDart(options);
931
+ });
932
+ async function runDart(options) {
933
+ await generate2(await loadSpec(options.spec), {
934
+ output: options.output,
935
+ mode: options.mode || "full",
936
+ name: options.name,
937
+ pagination: typeof options.pagination === "string" ? parsePagination(parseDotConfig(options.pagination ?? "true")) : options.pagination,
938
+ formatCode: ({ output }) => {
939
+ if (options.formatter) {
940
+ const [command, ...args] = options.formatter.split(" ");
941
+ execFile(command, args, {
942
+ env: { ...process.env, SDK_IT_OUTPUT: output }
943
+ });
944
+ } else {
945
+ execSync(`dart format ${shellEnv("SDK_IT_OUTPUT")}`, {
946
+ env: { ...process.env, SDK_IT_OUTPUT: output },
947
+ stdio: options.verbose ? "inherit" : "pipe"
948
+ });
949
+ }
950
+ }
951
+ });
952
+ }
953
+
954
+ // packages/cli/src/lib/generators/python.ts
955
+ import { Command as Command4 } from "commander";
956
+ import { execFile as execFile2, execSync as execSync2 } from "node:child_process";
957
+
958
+ // packages/python/dist/index.js
959
+ import { readdir as readdir2 } from "node:fs/promises";
960
+ import { join as join7 } from "node:path";
961
+ import { snakecase as snakecase2 } from "stringcase";
962
+ import { isEmpty, isRef as isRef2, pascalcase as pascalcase2 } from "@sdk-it/core";
963
+ import {
964
+ createWriterProxy,
965
+ writeFiles
966
+ } from "@sdk-it/core/file-system.js";
967
+ import {
968
+ cleanFiles,
969
+ forEachOperation,
970
+ isSuccessStatusCode,
971
+ parseJsonContentType,
972
+ readWriteMetadata,
973
+ toIR
974
+ } from "@sdk-it/spec";
975
+ import { snakecase } from "stringcase";
976
+ import { isRef, notRef, parseRef, pascalcase } from "@sdk-it/core";
977
+ import { isPrimitiveSchema } from "@sdk-it/spec";
978
+ var dispatcher_default = `"""HTTP dispatcher for making API requests."""
979
+
980
+ import asyncio
981
+ import logging
982
+ from typing import Any, Dict, List, Optional, Union
983
+ from urllib.parse import urljoin, urlparse
984
+
985
+ import httpx
986
+ from pydantic import BaseModel
987
+
988
+ from .interceptors import Interceptor
989
+ from .responses import ApiResponse, ErrorResponse
990
+
991
+
992
+ class RequestConfig(BaseModel):
993
+ """Configuration for an HTTP request."""
994
+
995
+ method: str
996
+ url: str
997
+ headers: Optional[Dict[str, str]] = None
998
+ params: Optional[Dict[str, Any]] = None
999
+ json_data: Optional[Dict[str, Any]] = None
1000
+ form_data: Optional[Dict[str, Any]] = None
1001
+ files: Optional[Dict[str, Any]] = None
1002
+ timeout: Optional[Union[float, httpx.Timeout]] = None
1003
+
1004
+ class Config:
1005
+ """Pydantic configuration."""
1006
+ arbitrary_types_allowed = True
1007
+
1008
+
1009
+ class Dispatcher:
1010
+ """HTTP client dispatcher with interceptor support."""
1011
+
1012
+ def __init__(
1013
+ self,
1014
+ interceptors: Optional[List[Interceptor]] = None,
1015
+ client: Optional[httpx.AsyncClient] = None,
1016
+ timeout: Optional[Union[float, httpx.Timeout]] = None
1017
+ ):
1018
+ """Initialize the dispatcher.
1019
+
1020
+ Args:
1021
+ interceptors: List of interceptors to apply to requests/responses
1022
+ client: Custom httpx.AsyncClient instance (creates default if None)
1023
+ timeout: Default timeout for requests
1024
+ """
1025
+ self.interceptors = interceptors or []
1026
+ self.client = client or httpx.AsyncClient(timeout=timeout)
1027
+ self.logger = logging.getLogger(__name__)
1028
+
1029
+ async def __aenter__(self):
1030
+ """Async context manager entry."""
1031
+ return self
1032
+
1033
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
1034
+ """Async context manager exit."""
1035
+ await self.client.aclose()
1036
+
1037
+ async def request(self, config: RequestConfig) -> httpx.Response:
1038
+ """Execute an HTTP request with interceptor processing.
1039
+
1040
+ Args:
1041
+ config: Request configuration
1042
+
1043
+ Returns:
1044
+ HTTP response after processing through interceptors
1045
+
1046
+ Raises:
1047
+ httpx.HTTPError: For HTTP-related errors
1048
+ ValueError: For invalid request configuration
1049
+ """
1050
+ # Process request interceptors
1051
+ processed_config = config
1052
+ for interceptor in self.interceptors:
1053
+ processed_config = await interceptor.process_request(processed_config)
1054
+
1055
+ # Prepare request arguments
1056
+ request_kwargs = self._prepare_request_kwargs(processed_config)
1057
+
1058
+ try:
1059
+ # Execute request
1060
+ response = await self.client.request(**request_kwargs)
1061
+
1062
+ # Process response interceptors (in reverse order)
1063
+ for interceptor in reversed(self.interceptors):
1064
+ response = await interceptor.process_response(response)
1065
+
1066
+ return response
1067
+
1068
+ except httpx.RequestError as e:
1069
+ self.logger.error(f"Request failed: {e}")
1070
+ raise
1071
+ except Exception as e:
1072
+ self.logger.error(f"Unexpected error during request: {e}")
1073
+ raise
1074
+
1075
+ def _prepare_request_kwargs(self, config: RequestConfig) -> Dict[str, Any]:
1076
+ """Prepare keyword arguments for httpx request.
1077
+
1078
+ Args:
1079
+ config: Request configuration
1080
+
1081
+ Returns:
1082
+ Dictionary of kwargs for httpx.request
1083
+
1084
+ Raises:
1085
+ ValueError: If request configuration is invalid
1086
+ """
1087
+ if not config.method:
1088
+ raise ValueError("Request method cannot be empty")
1089
+
1090
+ if not config.url:
1091
+ raise ValueError("Request URL cannot be empty")
1092
+
1093
+ request_kwargs = {
1094
+ 'method': config.method.upper(),
1095
+ 'url': config.url,
1096
+ 'headers': config.headers or {},
1097
+ 'params': config.params,
1098
+ 'timeout': config.timeout,
1099
+ }
1100
+
1101
+ # Handle different content types
1102
+ content_type_set = False
1103
+
1104
+ if config.json_data is not None:
1105
+ request_kwargs['json'] = config.json_data
1106
+ if 'Content-Type' not in request_kwargs['headers']:
1107
+ request_kwargs['headers']['Content-Type'] = 'application/json'
1108
+ content_type_set = True
1109
+
1110
+ elif config.form_data is not None:
1111
+ request_kwargs['data'] = config.form_data
1112
+ if 'Content-Type' not in request_kwargs['headers']:
1113
+ request_kwargs['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
1114
+ content_type_set = True
1115
+
1116
+ elif config.files is not None:
1117
+ request_kwargs['files'] = config.files
1118
+ # Don't set Content-Type for multipart/form-data - httpx will handle it automatically
1119
+ content_type_set = True
1120
+
1121
+ # Validate that only one content type is set
1122
+ content_fields = [config.json_data, config.form_data, config.files]
1123
+ non_none_count = sum(1 for field in content_fields if field is not None)
1124
+
1125
+ if non_none_count > 1:
1126
+ raise ValueError(
1127
+ "Only one of json_data, form_data, or files can be set in a single request"
1128
+ )
1129
+
1130
+ return request_kwargs
1131
+
1132
+ async def json(self, config: RequestConfig) -> httpx.Response:
1133
+ """Make a JSON request.
1134
+
1135
+ Args:
1136
+ config: Request configuration
1137
+
1138
+ Returns:
1139
+ HTTP response
1140
+ """
1141
+ return await self.request(config)
1142
+
1143
+ async def form(self, config: RequestConfig) -> httpx.Response:
1144
+ """Make a form-encoded request.
1145
+
1146
+ Args:
1147
+ config: Request configuration
1148
+
1149
+ Returns:
1150
+ HTTP response
1151
+ """
1152
+ return await self.request(config)
1153
+
1154
+ async def multipart(self, config: RequestConfig) -> httpx.Response:
1155
+ """Make a multipart/form-data request.
1156
+
1157
+ Args:
1158
+ config: Request configuration
1159
+
1160
+ Returns:
1161
+ HTTP response
1162
+ """
1163
+ return await self.request(config)
1164
+
1165
+ async def close(self):
1166
+ """Close the HTTP client and clean up resources."""
1167
+ await self.client.aclose()
1168
+
1169
+
1170
+ class Receiver:
1171
+ """Response processor with interceptor support."""
1172
+
1173
+ def __init__(
1174
+ self,
1175
+ interceptors: Optional[List[Interceptor]] = None,
1176
+ logger: Optional[logging.Logger] = None
1177
+ ):
1178
+ """Initialize the receiver.
1179
+
1180
+ Args:
1181
+ interceptors: List of interceptors to apply to responses
1182
+ logger: Custom logger instance
1183
+ """
1184
+ self.interceptors = interceptors or []
1185
+ self.logger = logger or logging.getLogger(__name__)
1186
+
1187
+ async def json(
1188
+ self,
1189
+ response: httpx.Response,
1190
+ success_model: Optional[type] = None,
1191
+ error_model: Optional[type] = None
1192
+ ) -> Any:
1193
+ """Process a JSON response.
1194
+
1195
+ Args:
1196
+ response: HTTP response to process
1197
+ success_model: Pydantic model for successful responses
1198
+ error_model: Pydantic model for error responses
1199
+
1200
+ Returns:
1201
+ Parsed response data, optionally as model instances
1202
+
1203
+ Raises:
1204
+ ErrorResponse: For HTTP error status codes
1205
+ ValueError: For response parsing errors
1206
+ """
1207
+ # Process response interceptors
1208
+ processed_response = response
1209
+ for interceptor in self.interceptors:
1210
+ processed_response = await interceptor.process_response(processed_response)
1211
+
1212
+ # Handle different status codes
1213
+ if 200 <= processed_response.status_code < 300:
1214
+ return await self._handle_success_response(
1215
+ processed_response, success_model
1216
+ )
1217
+ else:
1218
+ await self._handle_error_response(
1219
+ processed_response, error_model
1220
+ )
1221
+
1222
+ async def _handle_success_response(
1223
+ self,
1224
+ response: httpx.Response,
1225
+ success_model: Optional[type] = None
1226
+ ) -> Any:
1227
+ """Handle successful response.
1228
+
1229
+ Args:
1230
+ response: HTTP response
1231
+ success_model: Pydantic model for successful responses
1232
+
1233
+ Returns:
1234
+ Parsed response data
1235
+
1236
+ Raises:
1237
+ ValueError: For parsing errors
1238
+ """
1239
+ if not response.content:
1240
+ return None
1241
+
1242
+ try:
1243
+ data = response.json()
1244
+
1245
+ if success_model:
1246
+ if isinstance(data, list):
1247
+ return [success_model(**item) for item in data]
1248
+ else:
1249
+ return success_model(**data)
1250
+
1251
+ return data
1252
+
1253
+ except Exception as e:
1254
+ self.logger.error(f"Failed to parse success response: {e}")
1255
+ raise ValueError(f"Failed to parse response: {e}")
1256
+
1257
+ async def _handle_error_response(
1258
+ self,
1259
+ response: httpx.Response,
1260
+ error_model: Optional[type] = None
1261
+ ) -> None:
1262
+ """Handle error response.
1263
+
1264
+ Args:
1265
+ response: HTTP response
1266
+ error_model: Pydantic model for error responses
1267
+
1268
+ Raises:
1269
+ ErrorResponse: Always raises with error details
1270
+ """
1271
+ error_data = {}
1272
+
1273
+ if response.content:
1274
+ try:
1275
+ error_data = response.json()
1276
+ except Exception:
1277
+ # Fallback to text content if JSON parsing fails
1278
+ error_data = {'message': response.text}
1279
+
1280
+ if error_model:
1281
+ try:
1282
+ error = error_model(**error_data)
1283
+ raise ErrorResponse(error, response.status_code, dict(response.headers))
1284
+ except Exception as e:
1285
+ self.logger.warning(f"Failed to parse error with model {error_model}: {e}")
1286
+
1287
+ raise ErrorResponse(error_data, response.status_code, dict(response.headers))
1288
+
1289
+ async def stream(self, response: httpx.Response) -> httpx.Response:
1290
+ """Return streaming response as-is.
1291
+
1292
+ Args:
1293
+ response: HTTP response
1294
+
1295
+ Returns:
1296
+ The unmodified streaming response
1297
+ """
1298
+ return response
1299
+
1300
+ async def text(self, response: httpx.Response) -> str:
1301
+ """Get response as text.
1302
+
1303
+ Args:
1304
+ response: HTTP response
1305
+
1306
+ Returns:
1307
+ Response body as text
1308
+
1309
+ Raises:
1310
+ ErrorResponse: For HTTP error status codes
1311
+ """
1312
+ # Process response interceptors
1313
+ processed_response = response
1314
+ for interceptor in self.interceptors:
1315
+ processed_response = await interceptor.process_response(processed_response)
1316
+
1317
+ if 200 <= processed_response.status_code < 300:
1318
+ return processed_response.text
1319
+ else:
1320
+ error_data = {'message': processed_response.text}
1321
+ raise ErrorResponse(error_data, processed_response.status_code, dict(processed_response.headers))
1322
+
1323
+ async def bytes(self, response: httpx.Response) -> bytes:
1324
+ """Get response as bytes.
1325
+
1326
+ Args:
1327
+ response: HTTP response
1328
+
1329
+ Returns:
1330
+ Response body as bytes
1331
+
1332
+ Raises:
1333
+ ErrorResponse: For HTTP error status codes
1334
+ """
1335
+ # Process response interceptors
1336
+ processed_response = response
1337
+ for interceptor in self.interceptors:
1338
+ processed_response = await interceptor.process_response(processed_response)
1339
+
1340
+ if 200 <= processed_response.status_code < 300:
1341
+ return processed_response.content
1342
+ else:
1343
+ error_data = {'message': 'Binary response error'}
1344
+ raise ErrorResponse(error_data, processed_response.status_code, dict(processed_response.headers))
1345
+
1346
+
1347
+ # Convenience functions for common use cases
1348
+ async def quick_request(
1349
+ method: str,
1350
+ url: str,
1351
+ interceptors: Optional[List[Interceptor]] = None,
1352
+ **kwargs
1353
+ ) -> httpx.Response:
1354
+ """Make a quick HTTP request with interceptors.
1355
+
1356
+ Args:
1357
+ method: HTTP method
1358
+ url: Request URL
1359
+ interceptors: List of interceptors to apply
1360
+ **kwargs: Additional request configuration
1361
+
1362
+ Returns:
1363
+ HTTP response
1364
+ """
1365
+ config = RequestConfig(method=method, url=url, **kwargs)
1366
+
1367
+ async with Dispatcher(interceptors=interceptors) as dispatcher:
1368
+ return await dispatcher.request(config)
1369
+
1370
+
1371
+ async def quick_json_request(
1372
+ method: str,
1373
+ url: str,
1374
+ json_data: Optional[Dict[str, Any]] = None,
1375
+ interceptors: Optional[List[Interceptor]] = None,
1376
+ success_model: Optional[type] = None,
1377
+ error_model: Optional[type] = None,
1378
+ **kwargs
1379
+ ) -> Any:
1380
+ """Make a quick JSON HTTP request with interceptors.
1381
+
1382
+ Args:
1383
+ method: HTTP method
1384
+ url: Request URL
1385
+ json_data: JSON data to send
1386
+ interceptors: List of interceptors to apply
1387
+ success_model: Pydantic model for successful responses
1388
+ error_model: Pydantic model for error responses
1389
+ **kwargs: Additional request configuration
1390
+
1391
+ Returns:
1392
+ Parsed JSON response
1393
+ """
1394
+ config = RequestConfig(method=method, url=url, json_data=json_data, **kwargs)
1395
+
1396
+ async with Dispatcher(interceptors=interceptors) as dispatcher:
1397
+ response = await dispatcher.request(config)
1398
+ receiver = Receiver(interceptors=interceptors)
1399
+ return await receiver.json(response, success_model, error_model)
1400
+ `;
1401
+ var interceptors_default = `"""HTTP interceptors for request/response processing."""
1402
+
1403
+ import asyncio
1404
+ import logging
1405
+ import time
1406
+ from abc import ABC, abstractmethod
1407
+ from typing import Dict, Optional, List, Any, Union
1408
+ from urllib.parse import urljoin
1409
+
1410
+ import httpx
1411
+
1412
+ from .dispatcher import RequestConfig
1413
+
1414
+
1415
+ class Interceptor(ABC):
1416
+ """Base class for HTTP interceptors."""
1417
+
1418
+ @abstractmethod
1419
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
1420
+ """Process an outgoing request.
1421
+
1422
+ Args:
1423
+ config: The request configuration to process
1424
+
1425
+ Returns:
1426
+ The modified request configuration
1427
+ """
1428
+ pass
1429
+
1430
+ @abstractmethod
1431
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
1432
+ """Process an incoming response.
1433
+
1434
+ Args:
1435
+ response: The HTTP response to process
1436
+
1437
+ Returns:
1438
+ The processed response
1439
+ """
1440
+ pass
1441
+
1442
+
1443
+ class BaseUrlInterceptor(Interceptor):
1444
+ """Interceptor that prepends base URL to relative URLs."""
1445
+
1446
+ def __init__(self, base_url: str):
1447
+ """Initialize the base URL interceptor.
1448
+
1449
+ Args:
1450
+ base_url: The base URL to prepend to relative URLs
1451
+ """
1452
+ self.base_url = base_url.rstrip('/')
1453
+
1454
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
1455
+ """Prepend base URL if the request URL is relative.
1456
+
1457
+ Args:
1458
+ config: The request configuration
1459
+
1460
+ Returns:
1461
+ The modified request configuration with absolute URL
1462
+ """
1463
+ if not config.url.startswith(('http://', 'https://')):
1464
+ # Use urljoin for proper URL joining, ensuring single slash
1465
+ config.url = urljoin(self.base_url + '/', config.url.lstrip('/'))
1466
+ return config
1467
+
1468
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
1469
+ """Pass through response unchanged.
1470
+
1471
+ Args:
1472
+ response: The HTTP response
1473
+
1474
+ Returns:
1475
+ The unmodified response
1476
+ """
1477
+ return response
1478
+
1479
+
1480
+ class LoggingInterceptor(Interceptor):
1481
+ """Interceptor that logs requests and responses using Python's logging module."""
1482
+
1483
+ def __init__(
1484
+ self,
1485
+ enabled: bool = True,
1486
+ logger: Optional[logging.Logger] = None,
1487
+ log_level: int = logging.INFO,
1488
+ include_headers: bool = True,
1489
+ include_sensitive_headers: bool = False
1490
+ ):
1491
+ """Initialize the logging interceptor.
1492
+
1493
+ Args:
1494
+ enabled: Whether logging is enabled
1495
+ logger: Custom logger instance (creates default if None)
1496
+ log_level: Logging level to use
1497
+ include_headers: Whether to log request/response headers
1498
+ include_sensitive_headers: Whether to log sensitive headers like Authorization
1499
+ """
1500
+ self.enabled = enabled
1501
+ self.logger = logger or logging.getLogger(__name__)
1502
+ self.log_level = log_level
1503
+ self.include_headers = include_headers
1504
+ self.include_sensitive_headers = include_sensitive_headers
1505
+ self._sensitive_headers = {'authorization', 'x-api-key', 'cookie', 'set-cookie'}
1506
+
1507
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
1508
+ """Log outgoing request.
1509
+
1510
+ Args:
1511
+ config: The request configuration
1512
+
1513
+ Returns:
1514
+ The unmodified request configuration
1515
+ """
1516
+ if not self.enabled:
1517
+ return config
1518
+
1519
+ self.logger.log(self.log_level, f"\u2192 {config.method.upper()} {config.url}")
1520
+
1521
+ if self.include_headers and config.headers:
1522
+ for key, value in config.headers.items():
1523
+ if (key.lower() in self._sensitive_headers and
1524
+ not self.include_sensitive_headers):
1525
+ self.logger.log(self.log_level, f" {key}: [REDACTED]")
1526
+ else:
1527
+ self.logger.log(self.log_level, f" {key}: {value}")
1528
+
1529
+ return config
1530
+
1531
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
1532
+ """Log incoming response.
1533
+
1534
+ Args:
1535
+ response: The HTTP response
1536
+
1537
+ Returns:
1538
+ The unmodified response
1539
+ """
1540
+ if not self.enabled:
1541
+ return response
1542
+
1543
+ status_icon = "\u2713" if 200 <= response.status_code < 300 else "\u2717"
1544
+ self.logger.log(
1545
+ self.log_level,
1546
+ f"\u2190 {status_icon} {response.status_code} {response.reason_phrase or ''}"
1547
+ )
1548
+
1549
+ if self.include_headers and response.headers:
1550
+ for key, value in response.headers.items():
1551
+ if (key.lower() in self._sensitive_headers and
1552
+ not self.include_sensitive_headers):
1553
+ self.logger.log(self.log_level, f" {key}: [REDACTED]")
1554
+ else:
1555
+ self.logger.log(self.log_level, f" {key}: {value}")
1556
+
1557
+ return response
1558
+
1559
+
1560
+ class AuthInterceptor(Interceptor):
1561
+ """Interceptor that adds authentication headers."""
1562
+
1563
+ def __init__(
1564
+ self,
1565
+ token: Optional[str] = None,
1566
+ api_key: Optional[str] = None,
1567
+ api_key_header: str = 'X-API-Key',
1568
+ auth_type: str = 'Bearer'
1569
+ ):
1570
+ """Initialize the authentication interceptor.
1571
+
1572
+ Args:
1573
+ token: Bearer token for Authorization header
1574
+ api_key: API key value
1575
+ api_key_header: Header name for API key
1576
+ auth_type: Type of authentication (Bearer, Basic, etc.)
1577
+ """
1578
+ self.token = token
1579
+ self.api_key = api_key
1580
+ self.api_key_header = api_key_header
1581
+ self.auth_type = auth_type
1582
+
1583
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
1584
+ """Add authentication headers.
1585
+
1586
+ Args:
1587
+ config: The request configuration
1588
+
1589
+ Returns:
1590
+ The modified request configuration with auth headers
1591
+ """
1592
+ if config.headers is None:
1593
+ config.headers = {}
1594
+
1595
+ if self.token:
1596
+ config.headers['Authorization'] = f'{self.auth_type} {self.token}'
1597
+ elif self.api_key:
1598
+ config.headers[self.api_key_header] = self.api_key
1599
+
1600
+ return config
1601
+
1602
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
1603
+ """Pass through response unchanged.
1604
+
1605
+ Args:
1606
+ response: The HTTP response
1607
+
1608
+ Returns:
1609
+ The unmodified response
1610
+ """
1611
+ return response
1612
+
1613
+
1614
+ class RetryInterceptor(Interceptor):
1615
+ """Interceptor that retries failed requests with exponential backoff."""
1616
+
1617
+ def __init__(
1618
+ self,
1619
+ max_retries: int = 3,
1620
+ retry_delay: float = 1.0,
1621
+ backoff_factor: float = 2.0,
1622
+ retry_on_status: Optional[List[int]] = None,
1623
+ retry_on_exceptions: Optional[List[type]] = None
1624
+ ):
1625
+ """Initialize the retry interceptor.
1626
+
1627
+ Args:
1628
+ max_retries: Maximum number of retry attempts
1629
+ retry_delay: Initial delay between retries in seconds
1630
+ backoff_factor: Exponential backoff multiplier
1631
+ retry_on_status: HTTP status codes that should trigger retries
1632
+ retry_on_exceptions: Exception types that should trigger retries
1633
+ """
1634
+ self.max_retries = max_retries
1635
+ self.retry_delay = retry_delay
1636
+ self.backoff_factor = backoff_factor
1637
+ self.retry_on_status = retry_on_status or [500, 502, 503, 504, 408, 429]
1638
+ self.retry_on_exceptions = retry_on_exceptions or [
1639
+ httpx.TimeoutException,
1640
+ httpx.ConnectError,
1641
+ httpx.RemoteProtocolError
1642
+ ]
1643
+ self._original_request_func = None
1644
+ self.logger = logging.getLogger(__name__)
1645
+
1646
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
1647
+ """Store original request for potential retries.
1648
+
1649
+ Args:
1650
+ config: The request configuration
1651
+
1652
+ Returns:
1653
+ The unmodified request configuration
1654
+ """
1655
+ # Store the original config for retries
1656
+ self._original_config = config.model_copy() if hasattr(config, 'model_copy') else config
1657
+ return config
1658
+
1659
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
1660
+ """Check if response needs retry and handle accordingly.
1661
+
1662
+ Args:
1663
+ response: The HTTP response
1664
+
1665
+ Returns:
1666
+ The response (possibly after retries)
1667
+ """
1668
+ # For retry logic to work properly, it needs to be integrated at the dispatcher level
1669
+ # This is a simplified version that just passes through
1670
+ # In a full implementation, the retry logic would need access to the original request method
1671
+ return response
1672
+
1673
+ async def execute_with_retry(self, request_func, *args, **kwargs) -> httpx.Response:
1674
+ """Execute a request function with retry logic.
1675
+
1676
+ Args:
1677
+ request_func: Function that executes the HTTP request
1678
+ *args: Arguments to pass to request_func
1679
+ **kwargs: Keyword arguments to pass to request_func
1680
+
1681
+ Returns:
1682
+ The HTTP response after potential retries
1683
+
1684
+ Raises:
1685
+ The last exception encountered if all retries fail
1686
+ """
1687
+ last_exception = None
1688
+
1689
+ for attempt in range(self.max_retries + 1):
1690
+ try:
1691
+ response = await request_func(*args, **kwargs)
1692
+
1693
+ # Check if response status requires retry
1694
+ if response.status_code not in self.retry_on_status:
1695
+ return response
1696
+
1697
+ if attempt == self.max_retries:
1698
+ self.logger.warning(
1699
+ f"Max retries ({self.max_retries}) reached for request. "
1700
+ f"Final status: {response.status_code}"
1701
+ )
1702
+ return response
1703
+
1704
+ # Wait before retry
1705
+ delay = self.retry_delay * (self.backoff_factor ** attempt)
1706
+ self.logger.info(
1707
+ f"Retrying request (attempt {attempt + 1}/{self.max_retries + 1}) "
1708
+ f"after {delay:.2f}s due to status {response.status_code}"
1709
+ )
1710
+ await asyncio.sleep(delay)
1711
+
1712
+ except Exception as e:
1713
+ # Check if exception type requires retry
1714
+ if not any(isinstance(e, exc_type) for exc_type in self.retry_on_exceptions):
1715
+ raise e
1716
+
1717
+ last_exception = e
1718
+
1719
+ if attempt == self.max_retries:
1720
+ self.logger.error(
1721
+ f"Max retries ({self.max_retries}) reached. "
1722
+ f"Final exception: {type(e).__name__}: {e}"
1723
+ )
1724
+ raise e
1725
+
1726
+ # Wait before retry
1727
+ delay = self.retry_delay * (self.backoff_factor ** attempt)
1728
+ self.logger.info(
1729
+ f"Retrying request (attempt {attempt + 1}/{self.max_retries + 1}) "
1730
+ f"after {delay:.2f}s due to {type(e).__name__}: {e}"
1731
+ )
1732
+ await asyncio.sleep(delay)
1733
+
1734
+
1735
+ class UserAgentInterceptor(Interceptor):
1736
+ """Interceptor that adds a User-Agent header."""
1737
+
1738
+ def __init__(self, user_agent: str):
1739
+ """Initialize the User-Agent interceptor.
1740
+
1741
+ Args:
1742
+ user_agent: The User-Agent string to set
1743
+ """
1744
+ self.user_agent = user_agent
1745
+
1746
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
1747
+ """Add User-Agent header if not already present.
1748
+
1749
+ Args:
1750
+ config: The request configuration
1751
+
1752
+ Returns:
1753
+ The modified request configuration with User-Agent header
1754
+ """
1755
+ if config.headers is None:
1756
+ config.headers = {}
1757
+
1758
+ # Only set User-Agent if not already present (case-insensitive check)
1759
+ has_user_agent = any(
1760
+ key.lower() == 'user-agent'
1761
+ for key in config.headers.keys()
1762
+ )
1763
+
1764
+ if not has_user_agent:
1765
+ config.headers['User-Agent'] = self.user_agent
1766
+
1767
+ return config
1768
+
1769
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
1770
+ """Pass through response unchanged.
1771
+
1772
+ Args:
1773
+ response: The HTTP response
1774
+
1775
+ Returns:
1776
+ The unmodified response
1777
+ """
1778
+ return response
1779
+
1780
+
1781
+ class TimeoutInterceptor(Interceptor):
1782
+ """Interceptor that sets request timeouts."""
1783
+
1784
+ def __init__(self, timeout: Union[float, httpx.Timeout]):
1785
+ """Initialize the timeout interceptor.
1786
+
1787
+ Args:
1788
+ timeout: Timeout value in seconds or httpx.Timeout object
1789
+ """
1790
+ self.timeout = timeout
1791
+
1792
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
1793
+ """Set timeout for the request.
1794
+
1795
+ Args:
1796
+ config: The request configuration
1797
+
1798
+ Returns:
1799
+ The modified request configuration with timeout
1800
+ """
1801
+ if config.timeout is None:
1802
+ config.timeout = self.timeout
1803
+ return config
1804
+
1805
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
1806
+ """Pass through response unchanged.
1807
+
1808
+ Args:
1809
+ response: The HTTP response
1810
+
1811
+ Returns:
1812
+ The unmodified response
1813
+ """
1814
+ return response
1815
+
1816
+
1817
+ class RateLimitInterceptor(Interceptor):
1818
+ """Interceptor that implements client-side rate limiting."""
1819
+
1820
+ def __init__(self, max_requests: int, time_window: float = 60.0):
1821
+ """Initialize the rate limit interceptor.
1822
+
1823
+ Args:
1824
+ max_requests: Maximum number of requests allowed in the time window
1825
+ time_window: Time window in seconds
1826
+ """
1827
+ self.max_requests = max_requests
1828
+ self.time_window = time_window
1829
+ self.requests = []
1830
+ self._lock = asyncio.Lock()
1831
+
1832
+ async def process_request(self, config: RequestConfig) -> RequestConfig:
1833
+ """Apply rate limiting before request.
1834
+
1835
+ Args:
1836
+ config: The request configuration
1837
+
1838
+ Returns:
1839
+ The unmodified request configuration
1840
+ """
1841
+ async with self._lock:
1842
+ now = time.time()
1843
+
1844
+ # Remove requests outside the time window
1845
+ self.requests = [req_time for req_time in self.requests
1846
+ if now - req_time < self.time_window]
1847
+
1848
+ # Check if we've exceeded the rate limit
1849
+ if len(self.requests) >= self.max_requests:
1850
+ # Calculate how long to wait
1851
+ oldest_request = min(self.requests)
1852
+ wait_time = self.time_window - (now - oldest_request)
1853
+
1854
+ if wait_time > 0:
1855
+ await asyncio.sleep(wait_time)
1856
+
1857
+ # Record this request
1858
+ self.requests.append(now)
1859
+
1860
+ return config
1861
+
1862
+ async def process_response(self, response: httpx.Response) -> httpx.Response:
1863
+ """Pass through response unchanged.
1864
+
1865
+ Args:
1866
+ response: The HTTP response
1867
+
1868
+ Returns:
1869
+ The unmodified response
1870
+ """
1871
+ return response
1872
+
1873
+
1874
+ # Factory functions for convenient interceptor creation
1875
+ def create_base_url_interceptor(base_url: str) -> BaseUrlInterceptor:
1876
+ """Create a BaseUrlInterceptor instance.
1877
+
1878
+ Args:
1879
+ base_url: The base URL to prepend to relative URLs
1880
+
1881
+ Returns:
1882
+ Configured BaseUrlInterceptor instance
1883
+ """
1884
+ return BaseUrlInterceptor(base_url)
1885
+
1886
+
1887
+ def create_logging_interceptor(
1888
+ enabled: bool = True,
1889
+ log_level: int = logging.INFO,
1890
+ include_headers: bool = True,
1891
+ include_sensitive_headers: bool = False
1892
+ ) -> LoggingInterceptor:
1893
+ """Create a LoggingInterceptor instance.
1894
+
1895
+ Args:
1896
+ enabled: Whether logging is enabled
1897
+ log_level: Logging level to use
1898
+ include_headers: Whether to log headers
1899
+ include_sensitive_headers: Whether to log sensitive headers
1900
+
1901
+ Returns:
1902
+ Configured LoggingInterceptor instance
1903
+ """
1904
+ return LoggingInterceptor(
1905
+ enabled=enabled,
1906
+ log_level=log_level,
1907
+ include_headers=include_headers,
1908
+ include_sensitive_headers=include_sensitive_headers
1909
+ )
1910
+
1911
+
1912
+ def create_auth_interceptor(
1913
+ token: Optional[str] = None,
1914
+ api_key: Optional[str] = None,
1915
+ api_key_header: str = 'X-API-Key',
1916
+ auth_type: str = 'Bearer'
1917
+ ) -> AuthInterceptor:
1918
+ """Create an AuthInterceptor instance.
1919
+
1920
+ Args:
1921
+ token: Bearer token for Authorization header
1922
+ api_key: API key value
1923
+ api_key_header: Header name for API key
1924
+ auth_type: Type of authentication
1925
+
1926
+ Returns:
1927
+ Configured AuthInterceptor instance
1928
+ """
1929
+ return AuthInterceptor(
1930
+ token=token,
1931
+ api_key=api_key,
1932
+ api_key_header=api_key_header,
1933
+ auth_type=auth_type
1934
+ )
1935
+
1936
+
1937
+ def create_retry_interceptor(
1938
+ max_retries: int = 3,
1939
+ retry_delay: float = 1.0,
1940
+ backoff_factor: float = 2.0,
1941
+ retry_on_status: Optional[List[int]] = None
1942
+ ) -> RetryInterceptor:
1943
+ """Create a RetryInterceptor instance.
1944
+
1945
+ Args:
1946
+ max_retries: Maximum number of retry attempts
1947
+ retry_delay: Initial delay between retries in seconds
1948
+ backoff_factor: Exponential backoff multiplier
1949
+ retry_on_status: HTTP status codes that should trigger retries
1950
+
1951
+ Returns:
1952
+ Configured RetryInterceptor instance
1953
+ """
1954
+ return RetryInterceptor(
1955
+ max_retries=max_retries,
1956
+ retry_delay=retry_delay,
1957
+ backoff_factor=backoff_factor,
1958
+ retry_on_status=retry_on_status
1959
+ )
1960
+
1961
+
1962
+ def create_user_agent_interceptor(user_agent: str) -> UserAgentInterceptor:
1963
+ """Create a UserAgentInterceptor instance.
1964
+
1965
+ Args:
1966
+ user_agent: The User-Agent string to set
1967
+
1968
+ Returns:
1969
+ Configured UserAgentInterceptor instance
1970
+ """
1971
+ return UserAgentInterceptor(user_agent)
1972
+ `;
1973
+ var responses_default = `"""HTTP response models and exceptions."""
1974
+
1975
+ from typing import Any, Dict, Optional, Union
1976
+
1977
+ import httpx
1978
+ from pydantic import BaseModel
1979
+
1980
+
1981
+ class ApiResponse(BaseModel):
1982
+ """Base class for API responses."""
1983
+
1984
+ status_code: int
1985
+ headers: Dict[str, str]
1986
+ data: Any
1987
+
1988
+ class Config:
1989
+ """Pydantic configuration."""
1990
+ arbitrary_types_allowed = True
1991
+
1992
+
1993
+ class SuccessResponse(ApiResponse):
1994
+ """Represents a successful API response."""
1995
+
1996
+ def __init__(self, data: Any, status_code: int = 200, headers: Optional[Dict[str, str]] = None):
1997
+ """Initialize success response.
1998
+
1999
+ Args:
2000
+ data: Response data
2001
+ status_code: HTTP status code
2002
+ headers: Response headers
2003
+ """
2004
+ super().__init__(
2005
+ status_code=status_code,
2006
+ headers=headers or {},
2007
+ data=data
2008
+ )
2009
+
2010
+
2011
+ class ErrorResponse(Exception):
2012
+ """Exception raised for HTTP error responses."""
2013
+
2014
+ def __init__(
2015
+ self,
2016
+ data: Any,
2017
+ status_code: int,
2018
+ headers: Optional[Dict[str, str]] = None,
2019
+ message: Optional[str] = None
2020
+ ):
2021
+ """Initialize error response.
2022
+
2023
+ Args:
2024
+ data: Error response data
2025
+ status_code: HTTP status code
2026
+ headers: Response headers
2027
+ message: Custom error message
2028
+ """
2029
+ self.data = data
2030
+ self.status_code = status_code
2031
+ self.headers = headers or {}
2032
+ self.message = message or f"HTTP {status_code} Error"
2033
+
2034
+ super().__init__(self.message)
2035
+
2036
+ def __str__(self) -> str:
2037
+ """String representation of the error."""
2038
+ return f"ErrorResponse(status_code={self.status_code}, message='{self.message}')"
2039
+
2040
+ def __repr__(self) -> str:
2041
+ """Detailed string representation of the error."""
2042
+ return (
2043
+ f"ErrorResponse(status_code={self.status_code}, "
2044
+ f"message='{self.message}', data={self.data})"
2045
+ )
2046
+
2047
+
2048
+ class TimeoutError(ErrorResponse):
2049
+ """Exception raised for request timeouts."""
2050
+
2051
+ def __init__(self, message: str = "Request timed out"):
2052
+ """Initialize timeout error.
2053
+
2054
+ Args:
2055
+ message: Error message
2056
+ """
2057
+ super().__init__(
2058
+ data={'error': 'timeout'},
2059
+ status_code=408,
2060
+ message=message
2061
+ )
2062
+
2063
+
2064
+ class ConnectionError(ErrorResponse):
2065
+ """Exception raised for connection errors."""
2066
+
2067
+ def __init__(self, message: str = "Connection failed"):
2068
+ """Initialize connection error.
2069
+
2070
+ Args:
2071
+ message: Error message
2072
+ """
2073
+ super().__init__(
2074
+ data={'error': 'connection'},
2075
+ status_code=503,
2076
+ message=message
2077
+ )
2078
+
2079
+
2080
+ class BadRequestError(ErrorResponse):
2081
+ """Exception raised for 400 Bad Request errors."""
2082
+
2083
+ def __init__(self, data: Any = None, message: str = "Bad Request"):
2084
+ """Initialize bad request error.
2085
+
2086
+ Args:
2087
+ data: Error data
2088
+ message: Error message
2089
+ """
2090
+ super().__init__(
2091
+ data=data or {'error': 'bad_request'},
2092
+ status_code=400,
2093
+ message=message
2094
+ )
2095
+
2096
+
2097
+ class UnauthorizedError(ErrorResponse):
2098
+ """Exception raised for 401 Unauthorized errors."""
2099
+
2100
+ def __init__(self, data: Any = None, message: str = "Unauthorized"):
2101
+ """Initialize unauthorized error.
2102
+
2103
+ Args:
2104
+ data: Error data
2105
+ message: Error message
2106
+ """
2107
+ super().__init__(
2108
+ data=data or {'error': 'unauthorized'},
2109
+ status_code=401,
2110
+ message=message
2111
+ )
2112
+
2113
+
2114
+ class ForbiddenError(ErrorResponse):
2115
+ """Exception raised for 403 Forbidden errors."""
2116
+
2117
+ def __init__(self, data: Any = None, message: str = "Forbidden"):
2118
+ """Initialize forbidden error.
2119
+
2120
+ Args:
2121
+ data: Error data
2122
+ message: Error message
2123
+ """
2124
+ super().__init__(
2125
+ data=data or {'error': 'forbidden'},
2126
+ status_code=403,
2127
+ message=message
2128
+ )
2129
+
2130
+
2131
+ class NotFoundError(ErrorResponse):
2132
+ """Exception raised for 404 Not Found errors."""
2133
+
2134
+ def __init__(self, data: Any = None, message: str = "Not Found"):
2135
+ """Initialize not found error.
2136
+
2137
+ Args:
2138
+ data: Error data
2139
+ message: Error message
2140
+ """
2141
+ super().__init__(
2142
+ data=data or {'error': 'not_found'},
2143
+ status_code=404,
2144
+ message=message
2145
+ )
2146
+
2147
+
2148
+ class InternalServerError(ErrorResponse):
2149
+ """Exception raised for 500 Internal Server Error."""
2150
+
2151
+ def __init__(self, data: Any = None, message: str = "Internal Server Error"):
2152
+ """Initialize internal server error.
2153
+
2154
+ Args:
2155
+ data: Error data
2156
+ message: Error message
2157
+ """
2158
+ super().__init__(
2159
+ data=data or {'error': 'internal_server_error'},
2160
+ status_code=500,
2161
+ message=message
2162
+ )
2163
+
2164
+
2165
+ def create_error_from_response(response: httpx.Response) -> ErrorResponse:
2166
+ """Create appropriate error exception from HTTP response.
2167
+
2168
+ Args:
2169
+ response: HTTP response
2170
+
2171
+ Returns:
2172
+ Appropriate error exception
2173
+ """
2174
+ status_code = response.status_code
2175
+ headers = dict(response.headers)
2176
+
2177
+ # Try to parse error data
2178
+ try:
2179
+ data = response.json()
2180
+ except Exception:
2181
+ data = {'message': response.text}
2182
+
2183
+ # Create specific error types based on status code
2184
+ error_classes = {
2185
+ 400: BadRequestError,
2186
+ 401: UnauthorizedError,
2187
+ 403: ForbiddenError,
2188
+ 404: NotFoundError,
2189
+ 500: InternalServerError,
2190
+ }
2191
+
2192
+ error_class = error_classes.get(status_code, ErrorResponse)
2193
+
2194
+ if error_class == ErrorResponse:
2195
+ return ErrorResponse(data, status_code, headers)
2196
+ else:
2197
+ return error_class(data)
2198
+ `;
2199
+ function coerceObject(schema) {
2200
+ schema = structuredClone(schema);
2201
+ if (schema["x-properties"]) {
2202
+ schema.properties = {
2203
+ ...schema.properties ?? {},
2204
+ ...schema["x-properties"] ?? {}
2205
+ };
2206
+ }
2207
+ if (schema["x-required"]) {
2208
+ schema.required = Array.from(
2209
+ /* @__PURE__ */ new Set([
2210
+ ...Array.isArray(schema.required) ? schema.required : [],
2211
+ ...schema["x-required"] || []
2212
+ ])
2213
+ );
2214
+ }
2215
+ return schema;
2216
+ }
2217
+ var PythonEmitter = class {
2218
+ #spec;
2219
+ #emitHandler;
2220
+ #emitHistory = /* @__PURE__ */ new Set();
2221
+ #typeCache = /* @__PURE__ */ new Map();
2222
+ // Cache for resolved types
2223
+ #emit(name, content, schema) {
2224
+ if (this.#emitHistory.has(content)) {
2225
+ return;
2226
+ }
2227
+ this.#emitHistory.add(content);
2228
+ this.#emitHandler?.(name, content, schema);
2229
+ }
2230
+ constructor(spec) {
2231
+ this.#spec = spec;
2232
+ }
2233
+ onEmit(emit) {
2234
+ this.#emitHandler = emit;
2235
+ }
2236
+ #formatFieldName(name) {
2237
+ let fieldName = snakecase(name);
2238
+ const reservedKeywords = [
2239
+ "class",
2240
+ "def",
2241
+ "if",
2242
+ "else",
2243
+ "elif",
2244
+ "while",
2245
+ "for",
2246
+ "try",
2247
+ "except",
2248
+ "finally",
2249
+ "with",
2250
+ "as",
2251
+ "import",
2252
+ "from",
2253
+ "global",
2254
+ "nonlocal",
2255
+ "lambda",
2256
+ "yield",
2257
+ "return",
2258
+ "pass",
2259
+ "break",
2260
+ "continue",
2261
+ "True",
2262
+ "False",
2263
+ "None",
2264
+ "and",
2265
+ "or",
2266
+ "not",
2267
+ "in",
2268
+ "is"
2269
+ ];
2270
+ if (reservedKeywords.includes(fieldName)) {
2271
+ fieldName = `${fieldName}_`;
2272
+ }
2273
+ return fieldName;
2274
+ }
2275
+ #ref(ref) {
2276
+ const cacheKey = ref.$ref;
2277
+ const cached = this.#typeCache.get(cacheKey);
2278
+ if (cached) {
2279
+ return cached;
2280
+ }
2281
+ const refInfo = parseRef(ref.$ref);
2282
+ const refName = refInfo.model;
2283
+ const className = pascalcase(refName);
2284
+ const result = {
2285
+ type: className,
2286
+ content: "",
2287
+ use: className,
2288
+ fromJson: `${className}.parse_obj`,
2289
+ simple: false
2290
+ };
2291
+ this.#typeCache.set(cacheKey, result);
2292
+ return result;
2293
+ }
2294
+ #oneOf(variants, context) {
2295
+ const variantTypes = variants.map((variant) => this.handle(variant, context)).map((result) => result.type || "Any").filter((type, index, arr) => arr.indexOf(type) === index);
2296
+ if (variantTypes.length === 0) {
2297
+ return {
2298
+ type: "Any",
2299
+ content: "",
2300
+ use: "Any",
2301
+ fromJson: "Any",
2302
+ simple: true
2303
+ };
2304
+ }
2305
+ if (variantTypes.length === 1) {
2306
+ return {
2307
+ type: variantTypes[0],
2308
+ content: "",
2309
+ use: variantTypes[0],
2310
+ fromJson: variantTypes[0],
2311
+ simple: true
2312
+ };
2313
+ }
2314
+ const unionType = `Union[${variantTypes.join(", ")}]`;
2315
+ return {
2316
+ type: unionType,
2317
+ content: "",
2318
+ use: unionType,
2319
+ fromJson: unionType,
2320
+ simple: true
2321
+ };
2322
+ }
2323
+ #object(className, schema, context) {
2324
+ const { properties = {}, required = [] } = coerceObject(schema);
2325
+ const fields = [];
2326
+ let baseClass = "BaseModel";
2327
+ if (schema.allOf) {
2328
+ const bases = schema.allOf.filter(notRef).map((s) => this.handle(s, context)).filter((result) => result.type).map((result) => result.type);
2329
+ if (bases.length > 0 && bases[0]) {
2330
+ baseClass = bases[0];
2331
+ }
2332
+ }
2333
+ for (const [propName, propSchema] of Object.entries(properties)) {
2334
+ if (isRef(propSchema)) {
2335
+ this.#ref(propSchema);
2336
+ const refInfo = parseRef(propSchema.$ref);
2337
+ const refName = refInfo.model;
2338
+ const pythonType = pascalcase(refName);
2339
+ const fieldName = this.#formatFieldName(propName);
2340
+ const isRequired = required.includes(propName);
2341
+ const fieldType = isRequired ? pythonType : `Optional[${pythonType}]`;
2342
+ const defaultValue = isRequired ? "" : " = None";
2343
+ fields.push(` ${fieldName}: ${fieldType}${defaultValue}`);
2344
+ } else {
2345
+ const result = this.handle(propSchema, { ...context, name: propName });
2346
+ const fieldName = this.#formatFieldName(propName);
2347
+ const isRequired = required.includes(propName);
2348
+ let fieldType = result.type || "Any";
2349
+ if (!isRequired) {
2350
+ fieldType = `Optional[${fieldType}]`;
2351
+ }
2352
+ const defaultValue = isRequired ? "" : " = None";
2353
+ let fieldDef = ` ${fieldName}: ${fieldType}${defaultValue}`;
2354
+ if (fieldName !== propName) {
2355
+ fieldDef = ` ${fieldName}: ${fieldType} = Field(alias='${propName}'${defaultValue ? ", default=None" : ""})`;
2356
+ }
2357
+ if (propSchema.description) {
2358
+ fieldDef += ` # ${propSchema.description}`;
2359
+ }
2360
+ fields.push(fieldDef);
2361
+ }
2362
+ }
2363
+ if (schema.oneOf || schema.anyOf) {
2364
+ const unionResult = this.#oneOf(
2365
+ schema.oneOf || schema.anyOf || [],
2366
+ context
2367
+ );
2368
+ fields.push(` value: ${unionResult.type}`);
2369
+ }
2370
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
2371
+ const addlResult = this.handle(schema.additionalProperties, context);
2372
+ fields.push(
2373
+ ` additional_properties: Optional[Dict[str, ${addlResult.type || "Any"}]] = None`
2374
+ );
2375
+ }
2376
+ const docstring = schema.description ? ` """${schema.description}"""
2377
+ ` : "";
2378
+ let requestConfigMethod = "";
2379
+ if (schema["x-inputname"]) {
2380
+ requestConfigMethod = `
2381
+ def to_request_config(self, config: RequestConfig) -> RequestConfig:
2382
+ """Convert this input model to request configuration."""
2383
+ # Handle path parameters
2384
+ path_params = {}
2385
+ for key, value in self.dict(exclude_none=True).items():
2386
+ if key in config.url:
2387
+ path_params[key] = str(value)
2388
+ config.url = config.url.replace(f'{{{key}}}', str(value))
2389
+
2390
+ # Handle query parameters
2391
+ query_params = {k: v for k, v in self.dict(exclude_none=True).items()
2392
+ if k not in path_params}
2393
+ if query_params:
2394
+ config.params = query_params
2395
+
2396
+ return config
2397
+ `;
2398
+ }
2399
+ const content = `class ${className}(${baseClass}):
2400
+ ${docstring}${fields.length > 0 ? fields.join("\n") : " pass"}${requestConfigMethod}
2401
+ `;
2402
+ this.#emit(className, content, schema);
2403
+ return {
2404
+ type: className,
2405
+ content,
2406
+ use: className,
2407
+ fromJson: `${className}.parse_obj`,
2408
+ simple: false
2409
+ };
2410
+ }
2411
+ #primitive(schema) {
2412
+ const { type, format } = schema;
2413
+ const nullable = schema.nullable;
2414
+ let pythonType = "Any";
2415
+ switch (type) {
2416
+ case "string":
2417
+ if (format === "date-time") {
2418
+ pythonType = "datetime";
2419
+ } else if (format === "date") {
2420
+ pythonType = "date";
2421
+ } else if (format === "uuid") {
2422
+ pythonType = "UUID";
2423
+ } else if (format === "binary" || format === "byte") {
2424
+ pythonType = "bytes";
2425
+ } else {
2426
+ pythonType = "str";
2427
+ }
2428
+ break;
2429
+ case "integer":
2430
+ if (format === "int64") {
2431
+ pythonType = "int";
2432
+ } else {
2433
+ pythonType = "int";
2434
+ }
2435
+ break;
2436
+ case "number":
2437
+ pythonType = "float";
2438
+ break;
2439
+ case "boolean":
2440
+ pythonType = "bool";
2441
+ break;
2442
+ default:
2443
+ pythonType = "Any";
2444
+ }
2445
+ if (nullable) {
2446
+ pythonType = `Optional[${pythonType}]`;
2447
+ }
2448
+ return {
2449
+ type: pythonType,
2450
+ content: "",
2451
+ use: pythonType,
2452
+ fromJson: pythonType,
2453
+ simple: true,
2454
+ nullable
2455
+ };
2456
+ }
2457
+ #array(schema, context) {
2458
+ const itemsSchema = schema.items;
2459
+ if (!itemsSchema) {
2460
+ return {
2461
+ type: "List[Any]",
2462
+ content: "",
2463
+ use: "List[Any]",
2464
+ fromJson: "list",
2465
+ simple: true
2466
+ };
2467
+ }
2468
+ const itemsResult = this.handle(itemsSchema, context);
2469
+ const listType = `List[${itemsResult.type || "Any"}]`;
2470
+ return {
2471
+ type: listType,
2472
+ content: itemsResult.content,
2473
+ use: listType,
2474
+ fromJson: `List[${itemsResult.fromJson || itemsResult.type}]`,
2475
+ simple: true
2476
+ };
2477
+ }
2478
+ #enum(schema, _context) {
2479
+ const { enum: enumValues } = schema;
2480
+ if (!enumValues || enumValues.length === 0) {
2481
+ return this.#primitive(schema);
2482
+ }
2483
+ if (!_context.name || typeof _context.name !== "string") {
2484
+ throw new Error("Enum schemas must have a name in context");
2485
+ }
2486
+ const className = pascalcase(_context.name);
2487
+ const enumItems = enumValues.map((value, index) => {
2488
+ const name = typeof value === "string" ? value.toUpperCase().replace(/[^A-Z0-9]/g, "_") : `VALUE_${index}`;
2489
+ const pythonValue = typeof value === "string" ? `'${value}'` : String(value);
2490
+ return ` ${name} = ${pythonValue}`;
2491
+ });
2492
+ const content = `class ${className}(Enum):
2493
+ """Enumeration for ${_context.name}."""
2494
+ ${enumItems.join("\n")}
2495
+ `;
2496
+ this.#emit(className, content, schema);
2497
+ return {
2498
+ type: className,
2499
+ content,
2500
+ use: className,
2501
+ fromJson: className,
2502
+ simple: false
2503
+ };
2504
+ }
2505
+ #const(schema) {
2506
+ const { const: constValue } = schema;
2507
+ if (typeof constValue === "string") {
2508
+ return {
2509
+ type: `Literal['${constValue}']`,
2510
+ content: "",
2511
+ use: `Literal['${constValue}']`,
2512
+ fromJson: `'${constValue}'`,
2513
+ simple: true,
2514
+ literal: constValue
2515
+ };
2516
+ }
2517
+ return {
2518
+ type: `Literal[${JSON.stringify(constValue)}]`,
2519
+ content: "",
2520
+ use: `Literal[${JSON.stringify(constValue)}]`,
2521
+ fromJson: JSON.stringify(constValue),
2522
+ simple: true,
2523
+ literal: constValue
2524
+ };
2525
+ }
2526
+ handle(schema, context = {}) {
2527
+ if (isRef(schema)) {
2528
+ return this.#ref(schema);
2529
+ }
2530
+ if ("const" in schema && schema.const !== void 0) {
2531
+ return this.#const(schema);
2532
+ }
2533
+ if (schema.enum) {
2534
+ return this.#enum(schema, context);
2535
+ }
2536
+ if (schema.type === "array") {
2537
+ return this.#array(schema, context);
2538
+ }
2539
+ if (schema.oneOf || schema.anyOf) {
2540
+ return this.#oneOf(schema.oneOf || schema.anyOf || [], context);
2541
+ }
2542
+ if (schema.type === "object" || schema.properties || schema.allOf || schema.oneOf || schema.anyOf) {
2543
+ if (!context.name || typeof context.name !== "string") {
2544
+ throw new Error("Object schemas must have a name in context");
2545
+ }
2546
+ const className = pascalcase(context.name);
2547
+ return this.#object(className, schema, context);
2548
+ }
2549
+ if (isPrimitiveSchema(schema)) {
2550
+ return this.#primitive(schema);
2551
+ }
2552
+ return {
2553
+ type: "Any",
2554
+ content: "",
2555
+ use: "Any",
2556
+ fromJson: "Any",
2557
+ simple: true
2558
+ };
2559
+ }
2560
+ };
2561
+ async function generate3(openapi, settings) {
2562
+ const spec = await toIR({ spec: openapi }, true);
2563
+ const clientName = settings.name || "Client";
2564
+ const output = settings.output;
2565
+ const { writer, files: writtenFiles } = createWriterProxy(
2566
+ settings.writer ?? writeFiles,
2567
+ settings.output
2568
+ );
2569
+ settings.writer = writer;
2570
+ settings.readFolder ??= async (folder) => {
2571
+ const files = await readdir2(folder, { withFileTypes: true });
2572
+ return files.map((file) => ({
2573
+ fileName: file.name,
2574
+ filePath: join7(file.parentPath, file.name),
2575
+ isFolder: file.isDirectory()
2576
+ }));
2577
+ };
2578
+ const groups = {};
2579
+ forEachOperation(spec, (entry, operation) => {
2580
+ console.log(`Processing ${entry.method} ${entry.path}`);
2581
+ const group = groups[entry.tag] ??= {
2582
+ className: `${pascalcase2(entry.tag)}Api`,
2583
+ methods: []
2584
+ };
2585
+ const input2 = toInputs(spec, { entry, operation });
2586
+ const response = toOutput(spec, operation);
2587
+ const methodName = snakecase2(
2588
+ operation.operationId || `${entry.method}_${entry.path.replace(/[^a-zA-Z0-9]/g, "_")}`
2589
+ );
2590
+ const returnType = response ? response.returnType : "httpx.Response";
2591
+ const docstring = operation.summary || operation.description ? ` """${operation.summary || operation.description}"""` : "";
2592
+ group.methods.push(`
2593
+ async def ${methodName}(self${input2.haveInput ? `, input_data: ${input2.inputName}` : ""}) -> ${returnType}:
2594
+ ${docstring}
2595
+ config = RequestConfig(
2596
+ method='${entry.method.toUpperCase()}',
2597
+ url='${entry.path}',
2598
+ )
2599
+
2600
+ ${input2.haveInput ? "config = input_data.to_request_config(config)" : ""}
2601
+
2602
+ response = await self.dispatcher.${input2.contentType}(config)
2603
+ ${response ? `return await self.receiver.json(response, ${response.successModel || "None"}, ${response.errorModel || "None"})` : "return response"}
2604
+ `);
2605
+ });
2606
+ const emitter = new PythonEmitter(spec);
2607
+ const models = await serializeModels(spec, emitter);
2608
+ const apiClasses = Object.entries(groups).reduce(
2609
+ (acc, [name, { className, methods }]) => {
2610
+ const fileName = `api/${snakecase2(name)}_api.py`;
2611
+ const imports = [
2612
+ "from typing import Optional",
2613
+ "import httpx",
2614
+ "",
2615
+ "from ..http.dispatcher import Dispatcher, RequestConfig",
2616
+ "from ..http.responses import Receiver",
2617
+ "from ..inputs import *",
2618
+ "from ..outputs import *",
2619
+ "from ..models import *",
2620
+ ""
2621
+ ].join("\n");
2622
+ acc[fileName] = `${imports}
2623
+ class ${className}:
2624
+ """API client for ${name} operations."""
2625
+
2626
+ def __init__(self, dispatcher: Dispatcher, receiver: Receiver):
2627
+ self.dispatcher = dispatcher
2628
+ self.receiver = receiver
2629
+ ${methods.join("\n")}
2630
+ `;
2631
+ return acc;
2632
+ },
2633
+ {}
2634
+ );
2635
+ const apiImports = Object.keys(groups).map(
2636
+ (name) => `from .api.${snakecase2(name)}_api import ${pascalcase2(name)}Api`
2637
+ ).join("\n");
2638
+ const apiProperties = Object.keys(groups).map(
2639
+ (name) => ` self.${snakecase2(name)} = ${pascalcase2(name)}Api(dispatcher, receiver)`
2640
+ ).join("\n");
2641
+ const clientCode = `"""Main API client."""
2642
+
2643
+ from typing import Optional, List
2644
+ import httpx
2645
+
2646
+ ${apiImports}
2647
+ from .http.dispatcher import Dispatcher, RequestConfig
2648
+ from .http.responses import Receiver
2649
+ from .http.interceptors import (
2650
+ Interceptor,
2651
+ BaseUrlInterceptor,
2652
+ LoggingInterceptor,
2653
+ AuthInterceptor,
2654
+ UserAgentInterceptor,
2655
+ )
2656
+
2657
+
2658
+ class ${clientName}:
2659
+ """Main API client for the SDK."""
2660
+
2661
+ def __init__(
2662
+ self,
2663
+ base_url: str,
2664
+ token: Optional[str] = None,
2665
+ api_key: Optional[str] = None,
2666
+ api_key_header: str = 'X-API-Key',
2667
+ enable_logging: bool = False,
2668
+ user_agent: Optional[str] = None,
2669
+ custom_interceptors: Optional[List[Interceptor]] = None,
2670
+ ):
2671
+ """
2672
+ Initialize the API client.
2673
+
2674
+ Args:
2675
+ base_url: Base URL for the API
2676
+ token: Bearer token for authentication
2677
+ api_key: API key for authentication
2678
+ api_key_header: Header name for API key authentication
2679
+ enable_logging: Enable request/response logging
2680
+ user_agent: Custom User-Agent header
2681
+ custom_interceptors: Additional custom interceptors
2682
+ """
2683
+ self.base_url = base_url
2684
+
2685
+ # Build interceptor chain
2686
+ interceptors = []
2687
+
2688
+ # Base URL interceptor (always first)
2689
+ interceptors.append(BaseUrlInterceptor(base_url))
2690
+
2691
+ # Authentication interceptor
2692
+ if token or api_key:
2693
+ interceptors.append(AuthInterceptor(token=token, api_key=api_key, api_key_header=api_key_header))
2694
+
2695
+ # User agent interceptor
2696
+ if user_agent:
2697
+ interceptors.append(UserAgentInterceptor(user_agent))
2698
+
2699
+ # Logging interceptor
2700
+ if enable_logging:
2701
+ interceptors.append(LoggingInterceptor())
2702
+
2703
+ # Custom interceptors
2704
+ if custom_interceptors:
2705
+ interceptors.extend(custom_interceptors)
2706
+
2707
+ # Initialize dispatcher and receiver
2708
+ self.dispatcher = Dispatcher(interceptors)
2709
+ self.receiver = Receiver(interceptors)
2710
+
2711
+ # Initialize API clients
2712
+ ${apiProperties}
2713
+
2714
+ async def __aenter__(self):
2715
+ return self
2716
+
2717
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
2718
+ await self.close()
2719
+
2720
+ async def close(self):
2721
+ """Close the HTTP client."""
2722
+ await self.dispatcher.close()
2723
+ `;
2724
+ await settings.writer(output, {
2725
+ ...models,
2726
+ ...apiClasses,
2727
+ "client.py": clientCode,
2728
+ "http/dispatcher.py": dispatcher_default,
2729
+ "http/interceptors.py": interceptors_default,
2730
+ "http/responses.py": responses_default,
2731
+ "__init__.py": `"""SDK package."""
2732
+
2733
+ from .client import ${clientName}
2734
+
2735
+ __all__ = ['${clientName}']
2736
+ `
2737
+ });
2738
+ if (settings.mode === "full") {
2739
+ const requirements = `# HTTP client
2740
+ httpx>=0.24.0,<1.0.0
2741
+
2742
+ # Data validation and serialization
2743
+ pydantic>=2.0.0,<3.0.0
2744
+
2745
+ # Enhanced type hints
2746
+ typing-extensions>=4.0.0
2747
+
2748
+ # Optional: For better datetime handling
2749
+ python-dateutil>=2.8.0
2750
+ `;
2751
+ await settings.writer(output, {
2752
+ "requirements.txt": requirements
2753
+ });
2754
+ }
2755
+ const metadata = await readWriteMetadata(
2756
+ settings.output,
2757
+ Array.from(writtenFiles)
2758
+ );
2759
+ if (settings.cleanup !== false && writtenFiles.size > 0) {
2760
+ await cleanFiles(metadata.content, settings.output, [
2761
+ "/__init__.py",
2762
+ "requirements.txt",
2763
+ "/metadata.json"
2764
+ ]);
2765
+ }
2766
+ await settings.writer(output, {
2767
+ "models/__init__.py": await generateModuleInit(
2768
+ join7(output, "models"),
2769
+ settings.readFolder
2770
+ ),
2771
+ "inputs/__init__.py": await generateModuleInit(
2772
+ join7(output, "inputs"),
2773
+ settings.readFolder
2774
+ ),
2775
+ "outputs/__init__.py": await generateModuleInit(
2776
+ join7(output, "outputs"),
2777
+ settings.readFolder
2778
+ ),
2779
+ "api/__init__.py": await generateModuleInit(
2780
+ join7(output, "api"),
2781
+ settings.readFolder
2782
+ ),
2783
+ "http/__init__.py": `"""HTTP utilities."""
2784
+
2785
+ from .dispatcher import Dispatcher, RequestConfig
2786
+ from .interceptors import *
2787
+ from .responses import *
2788
+
2789
+ __all__ = [
2790
+ 'Dispatcher',
2791
+ 'RequestConfig',
2792
+ 'ApiResponse',
2793
+ 'ErrorResponse',
2794
+ 'Interceptor',
2795
+ 'BaseUrlInterceptor',
2796
+ 'LoggingInterceptor',
2797
+ 'AuthInterceptor',
2798
+ ]
2799
+ `
2800
+ });
2801
+ if (settings.formatCode) {
2802
+ await settings.formatCode({ output: settings.output });
2803
+ }
2804
+ }
2805
+ async function generateModuleInit(folder, readFolder) {
2806
+ try {
2807
+ const files = await readFolder(folder);
2808
+ const pyFiles = files.filter(
2809
+ (file) => file.fileName.endsWith(".py") && file.fileName !== "__init__.py"
2810
+ ).map((file) => file.fileName.replace(".py", ""));
2811
+ if (pyFiles.length === 0) {
2812
+ return '"""Package module."""\n';
2813
+ }
2814
+ const imports = pyFiles.map((name) => `from .${name} import *`).join("\n");
2815
+ return `"""Package module."""
2816
+
2817
+ ${imports}
2818
+ `;
2819
+ } catch {
2820
+ return '"""Package module."""\n';
2821
+ }
2822
+ }
2823
+ function toInputs(spec, { entry, operation }) {
2824
+ const inputName = entry.inputName || "Input";
2825
+ const haveInput = !isEmpty(operation.parameters) || !isEmpty(operation.requestBody);
2826
+ let contentType = "json";
2827
+ if (operation.requestBody && !isRef2(operation.requestBody)) {
2828
+ const content = operation.requestBody.content;
2829
+ if (content) {
2830
+ const contentTypes = Object.keys(content);
2831
+ if (contentTypes.some((type) => type.includes("multipart"))) {
2832
+ contentType = "multipart";
2833
+ } else if (contentTypes.some((type) => type.includes("form"))) {
2834
+ contentType = "form";
2835
+ }
2836
+ }
2837
+ }
2838
+ return {
2839
+ inputName,
2840
+ haveInput,
2841
+ contentType
2842
+ };
2843
+ }
2844
+ function toOutput(spec, operation) {
2845
+ if (!operation.responses) {
2846
+ return null;
2847
+ }
2848
+ const successResponse = Object.entries(operation.responses).find(
2849
+ ([code]) => isSuccessStatusCode(Number(code))
2850
+ );
2851
+ if (!successResponse) {
2852
+ return null;
2853
+ }
2854
+ const [, response] = successResponse;
2855
+ if (isRef2(response)) {
2856
+ return null;
2857
+ }
2858
+ const content = response.content;
2859
+ if (!content) {
2860
+ return { returnType: "None", successModel: null, errorModel: null };
2861
+ }
2862
+ const jsonContent = Object.entries(content).find(
2863
+ ([type]) => parseJsonContentType(type)
2864
+ );
2865
+ if (!jsonContent) {
2866
+ return {
2867
+ returnType: "httpx.Response",
2868
+ successModel: null,
2869
+ errorModel: null
2870
+ };
2871
+ }
2872
+ const [, mediaType] = jsonContent;
2873
+ const schema = mediaType.schema;
2874
+ if (!schema || isRef2(schema)) {
2875
+ return { returnType: "Any", successModel: null, errorModel: null };
2876
+ }
2877
+ const emitter = new PythonEmitter(spec);
2878
+ const result = emitter.handle(schema, {});
2879
+ return {
2880
+ returnType: result.type || "Any",
2881
+ successModel: result.type,
2882
+ errorModel: null
2883
+ // TODO: Handle error models
2884
+ };
2885
+ }
2886
+ async function serializeModels(spec, emitter) {
2887
+ const models = {};
2888
+ const standardImports = [
2889
+ "from typing import Any, Dict, List, Optional, Union, Literal",
2890
+ "from pydantic import BaseModel, Field",
2891
+ "from datetime import datetime, date",
2892
+ "from uuid import UUID",
2893
+ "from enum import Enum"
2894
+ ].join("\n");
2895
+ emitter.onEmit((name, content, schema) => {
2896
+ const fullContent = `${standardImports}
2897
+ ${schema["x-inputname"] ? "from ..http.dispatcher import RequestConfig" : ""}
2898
+
2899
+
2900
+ ${content}`;
2901
+ if (schema["x-inputname"]) {
2902
+ models[`inputs/${snakecase2(name)}.py`] = fullContent;
2903
+ } else if (schema["x-response-name"]) {
2904
+ models[`outputs/${snakecase2(name)}.py`] = fullContent;
2905
+ } else {
2906
+ models[`models/${snakecase2(name)}.py`] = fullContent;
2907
+ }
2908
+ });
2909
+ if (spec.components?.schemas) {
2910
+ for (const [name, schema] of Object.entries(spec.components.schemas)) {
2911
+ if (!isRef2(schema)) {
2912
+ emitter.handle(schema, { name });
2913
+ }
2914
+ }
2915
+ }
2916
+ return models;
2917
+ }
2918
+
2919
+ // packages/cli/src/lib/generators/python.ts
2920
+ import { loadSpec as loadSpec2, toIR as toIR2 } from "@sdk-it/spec";
2921
+ var python_default = new Command4("python").description("Generate Python SDK").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(true)).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) => {
2922
+ await runPython(options);
2923
+ });
2924
+ async function runPython(options) {
2925
+ const spec = await toIR2({ spec: await loadSpec2(options.spec) }, true);
2926
+ await generate3(spec, {
2927
+ output: options.output,
2928
+ mode: options.mode || "full",
2929
+ name: options.name,
2930
+ formatCode: ({ output }) => {
2931
+ if (options.formatter) {
2932
+ const [command, ...args] = options.formatter.split(" ");
2933
+ execFile2(command, args, {
2934
+ env: { ...process.env, SDK_IT_OUTPUT: output }
2935
+ });
2936
+ } else {
2937
+ try {
2938
+ execSync2(`black ${shellEnv("SDK_IT_OUTPUT")}`, {
2939
+ env: { ...process.env, SDK_IT_OUTPUT: output },
2940
+ stdio: options.verbose ? "inherit" : "pipe"
2941
+ });
2942
+ } catch {
2943
+ try {
2944
+ execSync2(`ruff format ${shellEnv("SDK_IT_OUTPUT")}`, {
2945
+ env: { ...process.env, SDK_IT_OUTPUT: output },
2946
+ stdio: options.verbose ? "inherit" : "pipe"
2947
+ });
2948
+ } catch {
2949
+ if (options.verbose) {
2950
+ console.warn(
2951
+ "No Python formatter found (black or ruff). Skipping formatting."
2952
+ );
2953
+ }
2954
+ }
2955
+ }
2956
+ }
2957
+ }
2958
+ });
2959
+ }
2960
+
2961
+ // packages/cli/src/lib/generators/readme.ts
2962
+ import { Command as Command5 } from "commander";
2963
+ import { writeFile as writeFile5 } from "node:fs/promises";
2964
+ import { toReadme } from "@sdk-it/readme";
2965
+ import { loadSpec as loadSpec3, toIR as toIR3 } from "@sdk-it/spec";
2966
+ var readme_default = new Command5("readme").description("Generate README").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(true)).action(async (options) => {
2967
+ await runReadme(options.spec, options.output);
2968
+ });
2969
+ async function runReadme(specFile, output) {
2970
+ const spec = await toIR3({ spec: await loadSpec3(specFile) });
2971
+ const content = toReadme(spec);
2972
+ await writeFile5(output, content, "utf-8");
2973
+ }
2974
+
2975
+ // packages/cli/src/lib/generators/typescript.ts
2976
+ import { Command as Command6, Option as Option2 } from "commander";
2977
+ import { publish } from "libnpmpublish";
2978
+ import { execFile as execFile3, execSync as execSync3, spawnSync } from "node:child_process";
2979
+ import { readFile as readFile4 } from "node:fs/promises";
2980
+ import { tmpdir } from "node:os";
2981
+ import { join as join8 } from "node:path";
2982
+ import getAuthToken from "registry-auth-token";
2983
+ import { writeFiles as writeFiles2 } from "@sdk-it/core/file-system.js";
2984
+ import { loadSpec as loadSpec4 } from "@sdk-it/spec";
2985
+ import { generate as generate4 } from "@sdk-it/typescript";
2986
+ var typescript_default = new Command6("typescript").alias("ts").description("Generate TypeScript SDK").addOption(specOption.makeOptionMandatory(true)).addOption(outputOption.makeOptionMandatory(false)).option(
2987
+ "--useTsExtension [value]",
2988
+ "Use .ts extension for generated files",
2989
+ (value) => value === "false" ? false : true,
2990
+ true
2991
+ ).option(
2992
+ "-m, --mode <mode>",
2993
+ "full: generate a full project including package.json and tsconfig.json. useful for monorepo/workspaces minimal: generate only the client sdk"
2994
+ ).option("-n, --name <name>", "Name of the generated client", "Client").option(
2995
+ "-f, --framework <framework>",
2996
+ "Framework that is integrating with the SDK"
2997
+ ).option("--formatter <formatter>", "Formatter to use for the generated code").option(
2998
+ "--install",
2999
+ "Install dependencies using npm (only in full mode)",
3000
+ true
3001
+ ).option(
3002
+ "--readme <readme>",
3003
+ "Generate a README file",
3004
+ (value) => value === "false" ? false : true,
3005
+ true
3006
+ ).option("--no-default-formatter", "Do not use the default formatter").option("--no-install", "Do not install dependencies").option("-v, --verbose", "Verbose output", false).option(
3007
+ "--pagination <pagination>",
3008
+ 'Configure pagination (e.g., "false", "true", "guess=false")',
3009
+ "true"
3010
+ ).addOption(
3011
+ new Option2(
3012
+ "--publish <publish>",
3013
+ "Publish the SDK to a package registry (npm, github, or a custom registry)"
3014
+ ).hideHelp(true).makeOptionMandatory(false)
3015
+ ).action(async (options) => {
3016
+ await runTypescript(options);
3017
+ });
3018
+ async function runTypescript(options) {
3019
+ if (!options.publish && !options.output) {
3020
+ throw new Error("Error: --publish or --output option is required.");
3021
+ }
3022
+ const spec = await loadSpec4(options.spec);
3023
+ if (options.output) {
3024
+ await emitLocal(spec, {
3025
+ ...options,
3026
+ output: options.output
3027
+ });
3028
+ }
3029
+ if (options.publish) {
3030
+ await emitRemote(spec, {
3031
+ ...options,
3032
+ publish: options.publish
3033
+ });
3034
+ }
3035
+ }
3036
+ async function emitLocal(spec, options) {
3037
+ await generate4(spec, {
3038
+ writer: writeFiles2,
3039
+ output: options.output,
3040
+ mode: options.mode || "minimal",
3041
+ name: options.name,
3042
+ pagination: typeof options.pagination === "string" ? parsePagination(parseDotConfig(options.pagination ?? "true")) : options.pagination,
3043
+ style: {
3044
+ name: "github"
3045
+ },
3046
+ readme: options.readme,
3047
+ useTsExtension: options.useTsExtension,
3048
+ formatCode: ({ env, output }) => {
3049
+ if (options.formatter) {
3050
+ const [command, ...args] = options.formatter.split(" ");
3051
+ execFile3(command, args, {
3052
+ env: { ...env, SDK_IT_OUTPUT: output }
3053
+ });
3054
+ } else if (options.defaultFormatter) {
3055
+ spawnSync("npx", ["-y", "prettier", output, "--write"], {
3056
+ env: {
3057
+ ...env,
3058
+ SDK_IT_OUTPUT: output
3059
+ },
3060
+ stdio: options.verbose ? "inherit" : "pipe"
3061
+ });
3062
+ }
3063
+ }
3064
+ });
3065
+ if (options.install && options.mode === "full") {
3066
+ console.log("Installing dependencies...");
3067
+ execSync3("npm install", {
3068
+ cwd: options.output,
3069
+ stdio: options.verbose ? "inherit" : "pipe"
3070
+ });
3071
+ }
3072
+ }
3073
+ async function emitRemote(spec, options) {
3074
+ const registry = options.publish === "npm" ? "https://registry.npmjs.org/" : options.publish === "github" ? "https://npm.pkg.github.com/" : options.publish;
3075
+ console.log("Publishing to registry:", registry);
3076
+ const path = join8(tmpdir(), crypto.randomUUID());
3077
+ await emitLocal(spec, {
3078
+ ...options,
3079
+ output: path,
3080
+ install: false,
3081
+ mode: "full"
3082
+ });
3083
+ const manifest = JSON.parse(
3084
+ await readFile4(join8(path, "package.json"), "utf-8")
3085
+ );
3086
+ const registryUrl = new URL(registry);
3087
+ const npmrc = process.env.NPM_TOKEN ? {
3088
+ npmrc: {
3089
+ registry,
3090
+ [`//${registryUrl.hostname}:_authToken`]: process.env.NPM_TOKEN
3091
+ }
3092
+ } : registry;
3093
+ const auth = getAuthToken(npmrc);
3094
+ if (!auth || !auth.token) {
3095
+ throw new Error(
3096
+ "No npm auth token found in .npmrc or environment. please provide NPM_TOKEN."
3097
+ );
3098
+ }
3099
+ const packResult = execSync3("npm pack --pack-destination .", { cwd: path });
3100
+ const [tgzName] = packResult.toString().trim().split("\n");
3101
+ await publish(manifest, await readFile4(join8(path, tgzName)), {
3102
+ registry,
3103
+ defaultTag: "latest",
3104
+ forceAuth: {
3105
+ token: auth.token
3106
+ },
3107
+ strictSSL: true,
3108
+ preferOnline: true
3109
+ });
3110
+ }
3111
+
3112
+ // packages/cli/src/lib/cli.ts
3113
+ var generate5 = new Command7("generate").option("-c, --config <path>", "Path to an SDK-IT configuration file").action(async (options) => {
3114
+ if (!options.config || options.config.endsWith(".ts")) {
3115
+ try {
3116
+ const config2 = await loadProjectConfig({ config: options.config });
3117
+ await generateProject(config2);
3118
+ console.log("Client generated successfully!");
3119
+ return;
3120
+ } catch (error) {
3121
+ if (options.config || !(error instanceof Error) || !error.message.startsWith("Could not find sdk-it.config.ts")) {
3122
+ throw error;
3123
+ }
3124
+ }
3125
+ }
3126
+ options.config ??= "sdk-it.json";
3127
+ const config = await readJson2(options.config);
3128
+ const promises = [];
3129
+ if (config.generators?.typescript) {
3130
+ promises.push(
3131
+ runTypescript({
3132
+ spec: config.generators.typescript.spec,
3133
+ output: config.generators.typescript.output,
3134
+ mode: config.generators.typescript.mode,
3135
+ name: config.generators.typescript.name,
3136
+ useTsExtension: config.generators.typescript.useTsExtension ?? true,
3137
+ install: config.generators.typescript.install ?? false,
3138
+ verbose: false,
3139
+ defaultFormatter: config.generators.typescript.defaultFormatter ?? true,
3140
+ readme: config.generators.typescript.readme ?? true,
3141
+ pagination: config.generators.typescript.pagination
3142
+ })
3143
+ );
3144
+ }
3145
+ if (config.generators?.python) {
3146
+ promises.push(
3147
+ runPython({
3148
+ spec: config.generators.python.spec,
3149
+ output: config.generators.python.output,
3150
+ mode: config.generators.python.mode,
3151
+ name: config.generators.python.name,
3152
+ verbose: false
3153
+ })
3154
+ );
3155
+ }
3156
+ if (config.generators?.dart) {
3157
+ promises.push(
3158
+ runDart({
3159
+ spec: config.generators.dart.spec,
3160
+ output: config.generators.dart.output,
3161
+ mode: config.generators.dart.mode,
3162
+ name: config.generators.dart.name,
3163
+ verbose: false,
3164
+ pagination: config.generators.dart.pagination
3165
+ })
3166
+ );
3167
+ }
3168
+ if (config.readme) {
3169
+ promises.push(runReadme(config.readme.spec, config.readme.output));
3170
+ }
3171
+ await Promise.all(promises);
3172
+ console.log("All configured generators completed successfully!");
3173
+ }).addCommand(typescript_default).addCommand(python_default).addCommand(dart_default).addCommand(apiref_default).addCommand(readme_default);
3174
+ var cli = program.description(`CLI tool to interact with SDK-IT.`).addCommand(generate5, { isDefault: true }).addCommand(init_default).addCommand(
3175
+ new Command7("_internal").action(() => {
3176
+ }),
3177
+ { hidden: true }
3178
+ ).parse(process.argv);
3179
+ //# sourceMappingURL=bin.js.map