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