@contractkit/plugin-python 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.turbo/turbo-build$colon$ci.log +35 -0
  2. package/.turbo/turbo-build.log +15 -0
  3. package/.turbo/turbo-test$colon$ci.log +59 -0
  4. package/.turbo/turbo-test.log +15 -0
  5. package/CHANGELOG.md +118 -0
  6. package/README.md +86 -0
  7. package/coverage/base.css +224 -0
  8. package/coverage/block-navigation.js +87 -0
  9. package/coverage/clover.xml +559 -0
  10. package/coverage/coverage-final.json +4 -0
  11. package/coverage/favicon.png +0 -0
  12. package/coverage/index.html +131 -0
  13. package/coverage/prettify.css +1 -0
  14. package/coverage/prettify.js +2 -0
  15. package/coverage/sort-arrow-sprite.png +0 -0
  16. package/coverage/sorter.js +210 -0
  17. package/coverage/src/codegen-client.ts.html +2071 -0
  18. package/coverage/src/codegen-models.ts.html +1360 -0
  19. package/coverage/src/index.html +131 -0
  20. package/coverage/tests/helpers.ts.html +667 -0
  21. package/coverage/tests/index.html +116 -0
  22. package/dist/codegen-client.d.ts +30 -0
  23. package/dist/codegen-client.d.ts.map +1 -0
  24. package/dist/codegen-models.d.ts +19 -0
  25. package/dist/codegen-models.d.ts.map +1 -0
  26. package/dist/index.d.ts +17 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +1046 -0
  29. package/dist/index.js.map +1 -0
  30. package/eslint.config.js +6 -0
  31. package/package.json +45 -0
  32. package/src/codegen-client.ts +662 -0
  33. package/src/codegen-models.ts +425 -0
  34. package/src/index.ts +143 -0
  35. package/tests/codegen-client.test.ts +361 -0
  36. package/tests/codegen-models.test.ts +295 -0
  37. package/tests/helpers.ts +194 -0
  38. package/tsconfig.json +9 -0
  39. package/vitest.config.ts +14 -0
package/dist/index.js ADDED
@@ -0,0 +1,1046 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/index.ts
5
+ import { resolve, join } from "path";
6
+
7
+ // src/codegen-models.ts
8
+ import { computeModelsWithInput, topoSortModels, collectExternalRefs, collectExternalInputRefs } from "@contractkit/core";
9
+ function generatePydanticModels(root, opts = {}) {
10
+ const externalRefs = collectExternalRefs(root);
11
+ const externalModelsWithInput = opts.modelsWithInput ?? /* @__PURE__ */ new Set();
12
+ const localModelsWithInput = computeModelsWithInput(root.models, externalModelsWithInput);
13
+ const allModelsWithInput = /* @__PURE__ */ new Set([
14
+ ...localModelsWithInput,
15
+ ...externalModelsWithInput
16
+ ]);
17
+ const externalInputRefs = allModelsWithInput.size > 0 ? collectExternalInputRefs(root, allModelsWithInput) : [];
18
+ const allExternalRefs = [
19
+ .../* @__PURE__ */ new Set([
20
+ ...externalRefs,
21
+ ...externalInputRefs
22
+ ])
23
+ ].sort();
24
+ const imports = new ImportTracker();
25
+ imports.add("pydantic", "BaseModel");
26
+ for (const model of root.models) {
27
+ if (model.type) {
28
+ scanTypeImports(model.type, imports);
29
+ } else {
30
+ for (const f of model.fields) {
31
+ scanTypeImports(f.type, imports);
32
+ }
33
+ }
34
+ }
35
+ const lines = [];
36
+ lines.push("# Auto-generated by contractkit-plugin-python-sdk. Do not edit manually.");
37
+ lines.push("from __future__ import annotations");
38
+ const crossFileImports = [];
39
+ for (const ref of allExternalRefs) {
40
+ const modulePath = opts.modelModulePaths?.get(ref);
41
+ if (modulePath && modulePath !== opts.currentModule) {
42
+ crossFileImports.push(`from ${modulePath} import ${ref}`);
43
+ }
44
+ }
45
+ const modelLines = [];
46
+ for (const model of topoSortModels(root.models)) {
47
+ modelLines.push("");
48
+ modelLines.push(...generateModel(model, allModelsWithInput, imports));
49
+ }
50
+ const stdlibLines = imports.render();
51
+ lines.push(...stdlibLines);
52
+ if (crossFileImports.length > 0) {
53
+ lines.push("");
54
+ lines.push(...crossFileImports);
55
+ }
56
+ lines.push(...modelLines);
57
+ lines.push("");
58
+ return lines.join("\n");
59
+ }
60
+ __name(generatePydanticModels, "generatePydanticModels");
61
+ var ImportTracker = class ImportTracker2 {
62
+ static {
63
+ __name(this, "ImportTracker");
64
+ }
65
+ groups = /* @__PURE__ */ new Map();
66
+ add(module, name) {
67
+ let s = this.groups.get(module);
68
+ if (!s) {
69
+ s = /* @__PURE__ */ new Set();
70
+ this.groups.set(module, s);
71
+ }
72
+ s.add(name);
73
+ }
74
+ has(module, name) {
75
+ return this.groups.get(module)?.has(name) ?? false;
76
+ }
77
+ render() {
78
+ const ORDER = [
79
+ "__future__",
80
+ "datetime",
81
+ "uuid",
82
+ "typing",
83
+ "pydantic"
84
+ ];
85
+ const lines = [];
86
+ lines.push("");
87
+ for (const mod of ORDER) {
88
+ const names = this.groups.get(mod);
89
+ if (!names || names.size === 0) continue;
90
+ const sorted = [
91
+ ...names
92
+ ].sort().join(", ");
93
+ lines.push(`from ${mod} import ${sorted}`);
94
+ }
95
+ for (const [mod, names] of this.groups) {
96
+ if (ORDER.includes(mod)) continue;
97
+ const sorted = [
98
+ ...names
99
+ ].sort().join(", ");
100
+ lines.push(`from ${mod} import ${sorted}`);
101
+ }
102
+ return lines;
103
+ }
104
+ };
105
+ function scanTypeImports(type, imports) {
106
+ switch (type.kind) {
107
+ case "scalar":
108
+ switch (type.name) {
109
+ case "date":
110
+ imports.add("datetime", "date");
111
+ break;
112
+ case "time":
113
+ imports.add("datetime", "time");
114
+ break;
115
+ case "datetime":
116
+ imports.add("datetime", "datetime");
117
+ break;
118
+ case "duration":
119
+ imports.add("datetime", "timedelta");
120
+ break;
121
+ case "uuid":
122
+ imports.add("uuid", "UUID");
123
+ break;
124
+ case "unknown":
125
+ case "json":
126
+ case "object":
127
+ imports.add("typing", "Any");
128
+ break;
129
+ }
130
+ break;
131
+ case "enum":
132
+ imports.add("typing", "Literal");
133
+ break;
134
+ case "union":
135
+ type.members.forEach((m) => scanTypeImports(m, imports));
136
+ break;
137
+ case "discriminatedUnion":
138
+ imports.add("typing", "Annotated");
139
+ imports.add("pydantic", "Field");
140
+ type.members.forEach((m) => scanTypeImports(m, imports));
141
+ break;
142
+ case "intersection":
143
+ imports.add("typing", "Any");
144
+ break;
145
+ case "array":
146
+ scanTypeImports(type.item, imports);
147
+ break;
148
+ case "tuple":
149
+ type.items.forEach((t) => scanTypeImports(t, imports));
150
+ break;
151
+ case "record":
152
+ scanTypeImports(type.key, imports);
153
+ scanTypeImports(type.value, imports);
154
+ break;
155
+ case "lazy":
156
+ scanTypeImports(type.inner, imports);
157
+ break;
158
+ case "inlineObject":
159
+ imports.add("typing", "Any");
160
+ break;
161
+ }
162
+ }
163
+ __name(scanTypeImports, "scanTypeImports");
164
+ function renderPyType(type, modelsWithInput, forInput = false) {
165
+ switch (type.kind) {
166
+ case "scalar":
167
+ return renderScalar(type.name);
168
+ case "enum":
169
+ return `Literal[${type.values.map((v) => JSON.stringify(v)).join(", ")}]`;
170
+ case "literal":
171
+ return typeof type.value === "string" ? JSON.stringify(type.value) : String(type.value);
172
+ case "array":
173
+ return `list[${renderPyType(type.item, modelsWithInput, forInput)}]`;
174
+ case "tuple":
175
+ if (type.items.length === 0) return "tuple[()]";
176
+ return `tuple[${type.items.map((t) => renderPyType(t, modelsWithInput, forInput)).join(", ")}]`;
177
+ case "record":
178
+ return `dict[${renderPyType(type.key, modelsWithInput, forInput)}, ${renderPyType(type.value, modelsWithInput, forInput)}]`;
179
+ case "union":
180
+ return type.members.map((m) => renderPyType(m, modelsWithInput, forInput)).join(" | ");
181
+ case "discriminatedUnion": {
182
+ const inner = type.members.map((m) => renderPyType(m, modelsWithInput, forInput)).join(" | ");
183
+ return `Annotated[${inner}, Field(discriminator=${JSON.stringify(type.discriminator)})]`;
184
+ }
185
+ case "intersection":
186
+ return "dict[str, Any]";
187
+ case "ref": {
188
+ if (forInput && modelsWithInput?.has(type.name)) {
189
+ return `${type.name}Input`;
190
+ }
191
+ return type.name;
192
+ }
193
+ case "inlineObject":
194
+ return "dict[str, Any]";
195
+ case "lazy":
196
+ return renderPyType(type.inner, modelsWithInput, forInput);
197
+ }
198
+ }
199
+ __name(renderPyType, "renderPyType");
200
+ function renderScalar(name) {
201
+ switch (name) {
202
+ case "string":
203
+ case "email":
204
+ case "url":
205
+ return "str";
206
+ case "number":
207
+ return "float";
208
+ case "int":
209
+ return "int";
210
+ case "bigint":
211
+ return "int";
212
+ case "boolean":
213
+ return "bool";
214
+ case "date":
215
+ return "date";
216
+ case "time":
217
+ return "time";
218
+ case "datetime":
219
+ return "datetime";
220
+ case "duration":
221
+ return "timedelta";
222
+ case "uuid":
223
+ return "UUID";
224
+ case "null":
225
+ return "None";
226
+ case "binary":
227
+ return "bytes";
228
+ case "unknown":
229
+ case "json":
230
+ case "object":
231
+ return "Any";
232
+ default:
233
+ return "Any";
234
+ }
235
+ }
236
+ __name(renderScalar, "renderScalar");
237
+ function toPythonFieldName(name) {
238
+ let result = name.replace(/[^a-zA-Z0-9_]/g, "_");
239
+ result = result.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
240
+ result = result.replace(/_+/g, "_").replace(/^_|_$/g, "");
241
+ if (/^\d/.test(result)) result = "f_" + result;
242
+ return result;
243
+ }
244
+ __name(toPythonFieldName, "toPythonFieldName");
245
+ function generateModel(model, allModelsWithInput, imports) {
246
+ if (model.type) {
247
+ return generateTypeAlias(model, allModelsWithInput, imports);
248
+ }
249
+ const needsInputSplit = model.fields.some((f) => f.visibility !== "normal") || allModelsWithInput.has(model.name);
250
+ if (needsInputSplit) {
251
+ return generateSplitModel(model, allModelsWithInput, imports);
252
+ }
253
+ return generateSimpleModel(model, allModelsWithInput, imports);
254
+ }
255
+ __name(generateModel, "generateModel");
256
+ function generateTypeAlias(model, allModelsWithInput, _) {
257
+ const lines = [];
258
+ if (model.description) lines.push(`# ${model.description}`);
259
+ if (model.deprecated) lines.push("# @deprecated");
260
+ lines.push(`${model.name} = ${renderPyType(model.type, allModelsWithInput)}`);
261
+ if (allModelsWithInput.has(model.name)) {
262
+ lines.push(`${model.name}Input = ${renderPyType(model.type, allModelsWithInput, true)}`);
263
+ }
264
+ return lines;
265
+ }
266
+ __name(generateTypeAlias, "generateTypeAlias");
267
+ function generateSimpleModel(model, allModelsWithInput, imports) {
268
+ const lines = [];
269
+ if (model.description) lines.push(`# ${model.description}`);
270
+ if (model.deprecated) lines.push("# @deprecated");
271
+ const baseList = model.bases && model.bases.length > 0 ? model.bases.join(", ") : "BaseModel";
272
+ lines.push(`class ${model.name}(${baseList}):`);
273
+ const fieldLines = renderFields(model.fields, allModelsWithInput, imports, false);
274
+ const needsConfig = model.fields.some((f) => toPythonFieldName(f.name) !== f.name);
275
+ if (needsConfig) {
276
+ imports.add("pydantic", "ConfigDict");
277
+ lines.push(` model_config = ConfigDict(populate_by_name=True)`);
278
+ lines.push("");
279
+ }
280
+ if (fieldLines.length === 0) {
281
+ lines.push(" pass");
282
+ } else {
283
+ lines.push(...fieldLines);
284
+ }
285
+ return lines;
286
+ }
287
+ __name(generateSimpleModel, "generateSimpleModel");
288
+ function generateSplitModel(model, allModelsWithInput, imports) {
289
+ const lines = [];
290
+ const readFields = model.fields.filter((f) => f.visibility !== "writeonly");
291
+ if (model.description) lines.push(`# ${model.description}`);
292
+ if (model.deprecated) lines.push("# @deprecated");
293
+ const readBaseList = model.bases && model.bases.length > 0 ? model.bases.join(", ") : "BaseModel";
294
+ lines.push(`class ${model.name}(${readBaseList}):`);
295
+ const readNeedsConfig = readFields.some((f) => toPythonFieldName(f.name) !== f.name);
296
+ if (readNeedsConfig) {
297
+ imports.add("pydantic", "ConfigDict");
298
+ lines.push(` model_config = ConfigDict(populate_by_name=True)`);
299
+ lines.push("");
300
+ }
301
+ const readFieldLines = renderFields(readFields, allModelsWithInput, imports, false);
302
+ if (readFieldLines.length === 0) {
303
+ lines.push(" pass");
304
+ } else {
305
+ lines.push(...readFieldLines);
306
+ }
307
+ lines.push("");
308
+ const writeFields = model.fields.filter((f) => f.visibility !== "readonly");
309
+ const inputBaseList = model.bases && model.bases.length > 0 ? model.bases.map((b) => allModelsWithInput.has(b) ? `${b}Input` : b).join(", ") : "BaseModel";
310
+ lines.push(`class ${model.name}Input(${inputBaseList}):`);
311
+ const writeNeedsConfig = writeFields.some((f) => toPythonFieldName(f.name) !== f.name);
312
+ if (writeNeedsConfig) {
313
+ imports.add("pydantic", "ConfigDict");
314
+ lines.push(` model_config = ConfigDict(populate_by_name=True)`);
315
+ lines.push("");
316
+ }
317
+ const writeFieldLines = renderFields(writeFields, allModelsWithInput, imports, true);
318
+ if (writeFieldLines.length === 0) {
319
+ lines.push(" pass");
320
+ } else {
321
+ lines.push(...writeFieldLines);
322
+ }
323
+ return lines;
324
+ }
325
+ __name(generateSplitModel, "generateSplitModel");
326
+ function renderFields(fields, allModelsWithInput, imports, forInput) {
327
+ const lines = [];
328
+ for (const f of fields) {
329
+ lines.push(...renderField(f, allModelsWithInput, imports, forInput));
330
+ }
331
+ return lines;
332
+ }
333
+ __name(renderFields, "renderFields");
334
+ function renderField(field, allModelsWithInput, imports, forInput) {
335
+ const lines = [];
336
+ const pyName = toPythonFieldName(field.name);
337
+ const needsAlias = pyName !== field.name;
338
+ let typeStr = renderPyType(field.type, allModelsWithInput, forInput);
339
+ if (field.nullable) typeStr = `${typeStr} | None`;
340
+ const isOptional = field.optional || field.default !== void 0;
341
+ const fieldAnnotations = [];
342
+ if (needsAlias) {
343
+ imports.add("pydantic", "Field");
344
+ fieldAnnotations.push(`alias=${JSON.stringify(field.name)}`);
345
+ }
346
+ if (field.default !== void 0) {
347
+ const def = typeof field.default === "string" ? JSON.stringify(field.default) : String(field.default);
348
+ fieldAnnotations.push(`default=${def}`);
349
+ }
350
+ if (field.deprecated) lines.push(` # @deprecated`);
351
+ if (field.description) lines.push(` # ${field.description}`);
352
+ let rhs;
353
+ if (fieldAnnotations.length > 0) {
354
+ imports.add("pydantic", "Field");
355
+ rhs = `Field(${fieldAnnotations.join(", ")})`;
356
+ } else if (isOptional) {
357
+ rhs = "None";
358
+ } else {
359
+ rhs = "";
360
+ }
361
+ const optSuffix = isOptional && !field.nullable ? ` | None` : "";
362
+ const fullType = typeStr + optSuffix;
363
+ if (rhs) {
364
+ lines.push(` ${pyName}: ${fullType} = ${rhs}`);
365
+ } else {
366
+ lines.push(` ${pyName}: ${fullType}`);
367
+ }
368
+ return lines;
369
+ }
370
+ __name(renderField, "renderField");
371
+ function deriveModelsModuleName(file) {
372
+ const base = file.split("/").pop()?.replace(/\.(op\.)?ck$/, "") ?? "models";
373
+ const clean = base.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
374
+ return `_models_${clean}`;
375
+ }
376
+ __name(deriveModelsModuleName, "deriveModelsModuleName");
377
+
378
+ // src/codegen-client.ts
379
+ import { resolveModifiers, classifyContentType } from "@contractkit/core";
380
+ function hasPublicOperations(root, includeInternal = false) {
381
+ for (const route of root.routes) {
382
+ for (const op of route.operations) {
383
+ if (includeInternal || !resolveModifiers(route, op).includes("internal")) return true;
384
+ }
385
+ }
386
+ return false;
387
+ }
388
+ __name(hasPublicOperations, "hasPublicOperations");
389
+ function generatePythonClient(root, opts = {}) {
390
+ const clientClassName = deriveClientClassName(root.file);
391
+ const { modelsWithInput } = opts;
392
+ const includeInternal = opts.includeInternal ?? false;
393
+ const referencedModels = collectReferencedModels(root, modelsWithInput, includeInternal);
394
+ const lines = [];
395
+ lines.push("# Auto-generated by contractkit-plugin-python-sdk. Do not edit manually.");
396
+ lines.push("from __future__ import annotations");
397
+ lines.push("");
398
+ const needsDatetime = referencedModels.has("__datetime__");
399
+ const needsDate = referencedModels.has("__date__");
400
+ const needsTime = referencedModels.has("__time__");
401
+ const needsUUID = referencedModels.has("__uuid__");
402
+ const needsAny = referencedModels.has("__any__");
403
+ if (needsDatetime || needsDate || needsTime) {
404
+ const dtParts = [];
405
+ if (needsDate) dtParts.push("date");
406
+ if (needsDatetime) dtParts.push("datetime");
407
+ if (needsTime) dtParts.push("time");
408
+ lines.push(`from datetime import ${dtParts.join(", ")}`);
409
+ }
410
+ if (needsUUID) lines.push("from uuid import UUID");
411
+ const publicOps = [];
412
+ for (const route of root.routes) {
413
+ for (const op of route.operations) {
414
+ if (!includeInternal && resolveModifiers(route, op).includes("internal")) continue;
415
+ publicOps.push({
416
+ route,
417
+ op
418
+ });
419
+ }
420
+ }
421
+ const opsWithRespHeaders = publicOps.filter(({ op }) => {
422
+ const primary = op.responses.find((r) => r.bodyType) ?? op.responses[0];
423
+ return (primary?.headers?.length ?? 0) > 0;
424
+ });
425
+ if (needsAny || opsWithRespHeaders.length > 0) {
426
+ const typingImports = [];
427
+ if (needsAny) typingImports.push("Any");
428
+ if (opsWithRespHeaders.length > 0) typingImports.push("TypedDict");
429
+ lines.push(`from typing import ${typingImports.join(", ")}`);
430
+ }
431
+ lines.push("from ._base_client import BaseClient, SdkError # noqa: F401");
432
+ const modelImportsByModule = /* @__PURE__ */ new Map();
433
+ for (const name of referencedModels) {
434
+ if (name.startsWith("__")) continue;
435
+ const modulePath = opts.modelModulePaths?.get(name);
436
+ if (modulePath) {
437
+ let s = modelImportsByModule.get(modulePath);
438
+ if (!s) {
439
+ s = /* @__PURE__ */ new Set();
440
+ modelImportsByModule.set(modulePath, s);
441
+ }
442
+ s.add(name);
443
+ }
444
+ }
445
+ for (const [mod, names] of [
446
+ ...modelImportsByModule
447
+ ].sort((a, b) => a[0].localeCompare(b[0]))) {
448
+ const sorted = [
449
+ ...names
450
+ ].sort().join(", ");
451
+ lines.push(`from ${mod} import ${sorted}`);
452
+ }
453
+ for (const { route, op } of opsWithRespHeaders) {
454
+ const primary = op.responses.find((r) => r.bodyType) ?? op.responses[0];
455
+ const className = `${snakeToPascal(deriveMethodName(op, route))}Headers`;
456
+ lines.push("");
457
+ lines.push("");
458
+ lines.push(`class ${className}(TypedDict, total=False):`);
459
+ for (const h of primary.headers) {
460
+ const pyName = toPythonFieldName(h.name);
461
+ const tag = h.optional ? "optional" : "required";
462
+ lines.push(` ${pyName}: str # ${h.name} (${tag})`);
463
+ }
464
+ }
465
+ lines.push("");
466
+ lines.push("");
467
+ lines.push(`class ${clientClassName}(BaseClient):`);
468
+ let hasAnyMethod = false;
469
+ for (const route of root.routes) {
470
+ for (const op of route.operations) {
471
+ const mods = resolveModifiers(route, op);
472
+ if (!includeInternal && mods.includes("internal")) continue;
473
+ hasAnyMethod = true;
474
+ lines.push("");
475
+ if (mods.includes("deprecated")) lines.push(" # @deprecated");
476
+ lines.push(...generateMethod(route, op, opts));
477
+ }
478
+ }
479
+ if (!hasAnyMethod) {
480
+ lines.push(" pass");
481
+ }
482
+ lines.push("");
483
+ return lines.join("\n");
484
+ }
485
+ __name(generatePythonClient, "generatePythonClient");
486
+ function generateMethod(route, op, opts) {
487
+ const lines = [];
488
+ const { modelsWithInput } = opts;
489
+ const methodName = deriveMethodName(op, route);
490
+ const httpMethod = op.method.toUpperCase();
491
+ const params = buildMethodParams(route, op, modelsWithInput);
492
+ const selfParam = "self";
493
+ const allParams = params.map((p) => {
494
+ if (p.optional) return `${p.name}: ${p.type} | None = None`;
495
+ return `${p.name}: ${p.type}`;
496
+ });
497
+ const paramStr = allParams.length > 0 ? `, ${allParams.join(", ")}` : "";
498
+ const primaryResponse = op.responses.find((r) => r.bodyType) ?? op.responses[0];
499
+ const isVoid = !primaryResponse?.bodyType;
500
+ const respCategory = primaryResponse?.contentType ? classifyContentType(primaryResponse.contentType) : "json";
501
+ const dataType = isVoid ? "None" : respCategory === "text" ? "str" : respCategory === "binary" ? "bytes" : renderPyType(primaryResponse.bodyType, modelsWithInput);
502
+ const isModelReturn = !isVoid && respCategory === "json" && isModelRef(primaryResponse.bodyType, modelsWithInput);
503
+ const isListModelReturn = !isVoid && respCategory === "json" && isListModelRef(primaryResponse.bodyType, modelsWithInput);
504
+ const respHeaders = primaryResponse?.headers ?? [];
505
+ const hasRespHeaders = respHeaders.length > 0;
506
+ const headersTypeName = hasRespHeaders ? `${snakeToPascal(methodName)}Headers` : "";
507
+ const returnType = hasRespHeaders ? isVoid ? headersTypeName : `tuple[${dataType}, ${headersTypeName}]` : dataType;
508
+ const desc = op.description ?? route.description;
509
+ if (op.name || desc) {
510
+ lines.push(` async def ${methodName}(${selfParam}${paramStr}) -> ${returnType}:`);
511
+ lines.push(` """`);
512
+ if (op.name) lines.push(` ${op.name}`);
513
+ if (desc) lines.push(` ${desc}`);
514
+ lines.push(` """`);
515
+ } else {
516
+ lines.push(` async def ${methodName}(${selfParam}${paramStr}) -> ${returnType}:`);
517
+ }
518
+ const urlExpr = buildUrlExpression(route.path);
519
+ const hasQuery = !!op.query;
520
+ const primaryBody = op.request?.bodies[0];
521
+ const hasBody = !!primaryBody;
522
+ const isMultipart = primaryBody?.contentType === "multipart/form-data";
523
+ const hasCustomHeaders = !!op.headers;
524
+ const fetchKwargs = [];
525
+ fetchKwargs.push(`method="${httpMethod}"`);
526
+ const reqCategory = primaryBody?.contentType ? classifyContentType(primaryBody.contentType) : "json";
527
+ if (hasBody) {
528
+ const bodyParam = params.find((p) => p.name === "body");
529
+ if (isMultipart) {
530
+ fetchKwargs.push("body=body");
531
+ } else if (reqCategory === "text" || reqCategory === "binary") {
532
+ fetchKwargs.push("body=body");
533
+ } else if (bodyParam?.isModel) {
534
+ fetchKwargs.push('body=body.model_dump(mode="json")');
535
+ } else {
536
+ fetchKwargs.push("body=body");
537
+ }
538
+ if (primaryBody.contentType !== "application/json") {
539
+ fetchKwargs.push(`content_type=${JSON.stringify(primaryBody.contentType)}`);
540
+ }
541
+ if (reqCategory === "text" || reqCategory === "binary") {
542
+ fetchKwargs.push(`body_kind="${reqCategory}"`);
543
+ }
544
+ }
545
+ if (respCategory === "text" || respCategory === "binary") {
546
+ fetchKwargs.push(`response_kind="${respCategory}"`);
547
+ }
548
+ if (hasQuery) {
549
+ fetchKwargs.push("params=query");
550
+ }
551
+ if (hasCustomHeaders) {
552
+ fetchKwargs.push("extra_headers=custom_headers");
553
+ }
554
+ const kwargsStr = fetchKwargs.length > 1 ? fetchKwargs.join(", ") : fetchKwargs[0] ?? "";
555
+ if (hasRespHeaders) {
556
+ lines.push(` result, _response_headers = await self._fetch_with_headers(${urlExpr}, ${kwargsStr})`);
557
+ lines.push(...buildHeadersDictLines(respHeaders, headersTypeName));
558
+ } else {
559
+ lines.push(` result = await self._fetch(${urlExpr}, ${kwargsStr})`);
560
+ }
561
+ if (isVoid) {
562
+ if (hasRespHeaders) {
563
+ lines.push(` return headers`);
564
+ } else {
565
+ lines.push(` return None`);
566
+ }
567
+ } else {
568
+ let dataExpr;
569
+ if (isListModelReturn) {
570
+ const innerType = getListItemType(primaryResponse.bodyType, modelsWithInput);
571
+ dataExpr = `[${innerType}.model_validate(item) for item in result]`;
572
+ } else if (isModelReturn) {
573
+ dataExpr = `${dataType}.model_validate(result)`;
574
+ } else {
575
+ dataExpr = "result";
576
+ }
577
+ if (hasRespHeaders) {
578
+ lines.push(` return ${dataExpr}, headers`);
579
+ } else {
580
+ lines.push(` return ${dataExpr}`);
581
+ }
582
+ }
583
+ return lines;
584
+ }
585
+ __name(generateMethod, "generateMethod");
586
+ function buildHeadersDictLines(headers, typeName) {
587
+ const lines = [];
588
+ lines.push(` headers: ${typeName} = {}`);
589
+ for (const h of headers) {
590
+ const pyName = toPythonFieldName(h.name);
591
+ lines.push(` if ${JSON.stringify(h.name.toLowerCase())} in _response_headers:`);
592
+ lines.push(` headers[${JSON.stringify(pyName)}] = _response_headers[${JSON.stringify(h.name.toLowerCase())}]`);
593
+ }
594
+ return lines;
595
+ }
596
+ __name(buildHeadersDictLines, "buildHeadersDictLines");
597
+ function snakeToPascal(s) {
598
+ return s.split("_").filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
599
+ }
600
+ __name(snakeToPascal, "snakeToPascal");
601
+ function buildUrlExpression(path) {
602
+ const hasBraces = /\{[a-zA-Z_][a-zA-Z0-9_]*\}/.test(path);
603
+ if (!hasBraces) return `"${path}"`;
604
+ const interpolated = path.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_m, name) => `{${name}}`);
605
+ return `f"${interpolated}"`;
606
+ }
607
+ __name(buildUrlExpression, "buildUrlExpression");
608
+ function buildMethodParams(route, op, modelsWithInput) {
609
+ const params = [];
610
+ if (route.params) {
611
+ if (route.params.kind === "params") {
612
+ for (const p of route.params.nodes) {
613
+ params.push({
614
+ name: toPythonFieldName(p.name),
615
+ type: renderPyType(p.type, modelsWithInput),
616
+ optional: false,
617
+ isModel: false
618
+ });
619
+ }
620
+ } else if (route.params.kind === "ref") {
621
+ const typeName = modelsWithInput?.has(route.params.name) ? `${route.params.name}Input` : route.params.name;
622
+ params.push({
623
+ name: "params",
624
+ type: typeName,
625
+ optional: false,
626
+ isModel: true
627
+ });
628
+ } else {
629
+ params.push({
630
+ name: "params",
631
+ type: renderPyType(route.params.node, modelsWithInput),
632
+ optional: false,
633
+ isModel: false
634
+ });
635
+ }
636
+ }
637
+ const primaryBody = op.request?.bodies[0];
638
+ if (primaryBody) {
639
+ const cat = classifyContentType(primaryBody.contentType);
640
+ if (cat === "multipart" || cat === "binary") {
641
+ params.push({
642
+ name: "body",
643
+ type: "bytes",
644
+ optional: false,
645
+ isModel: false
646
+ });
647
+ } else if (cat === "text") {
648
+ params.push({
649
+ name: "body",
650
+ type: "str",
651
+ optional: false,
652
+ isModel: false
653
+ });
654
+ } else {
655
+ const bodyType = renderInputPyType(primaryBody.bodyType, modelsWithInput);
656
+ const isModel = isModelRef(primaryBody.bodyType, modelsWithInput);
657
+ params.push({
658
+ name: "body",
659
+ type: bodyType,
660
+ optional: false,
661
+ isModel
662
+ });
663
+ }
664
+ }
665
+ if (op.query) {
666
+ const queryType = renderParamSourceType(op.query, modelsWithInput, true);
667
+ params.push({
668
+ name: "query",
669
+ type: queryType,
670
+ optional: true,
671
+ isModel: false
672
+ });
673
+ }
674
+ if (op.headers) {
675
+ const headersType = renderParamSourceType(op.headers, modelsWithInput, true);
676
+ params.push({
677
+ name: "custom_headers",
678
+ type: headersType,
679
+ optional: true,
680
+ isModel: false
681
+ });
682
+ }
683
+ return params;
684
+ }
685
+ __name(buildMethodParams, "buildMethodParams");
686
+ function renderParamSourceType(source, modelsWithInput, forInput = false) {
687
+ if (source.kind === "ref") {
688
+ const typeName = forInput && modelsWithInput?.has(source.name) ? `${source.name}Input` : source.name;
689
+ return typeName;
690
+ }
691
+ if (source.kind === "params") {
692
+ return `dict`;
693
+ }
694
+ return renderPyType(source.node, modelsWithInput, forInput);
695
+ }
696
+ __name(renderParamSourceType, "renderParamSourceType");
697
+ function renderInputPyType(type, modelsWithInput) {
698
+ return renderPyType(type, modelsWithInput, true);
699
+ }
700
+ __name(renderInputPyType, "renderInputPyType");
701
+ function isModelRef(type, modelsWithInput) {
702
+ if (type.kind === "ref") return /^[A-Z]/.test(type.name);
703
+ if (type.kind === "lazy") return isModelRef(type.inner, modelsWithInput);
704
+ return false;
705
+ }
706
+ __name(isModelRef, "isModelRef");
707
+ function isListModelRef(type, modelsWithInput) {
708
+ if (type.kind === "array") return isModelRef(type.item, modelsWithInput);
709
+ if (type.kind === "lazy") return isListModelRef(type.inner, modelsWithInput);
710
+ return false;
711
+ }
712
+ __name(isListModelRef, "isListModelRef");
713
+ function getListItemType(type, modelsWithInput) {
714
+ if (type.kind === "array") return renderPyType(type.item, modelsWithInput);
715
+ return "dict";
716
+ }
717
+ __name(getListItemType, "getListItemType");
718
+ function collectReferencedModels(root, modelsWithInput, includeInternal = false) {
719
+ const refs = /* @__PURE__ */ new Set();
720
+ for (const route of root.routes) {
721
+ for (const op of route.operations) {
722
+ if (!includeInternal && resolveModifiers(route, op).includes("internal")) continue;
723
+ if (route.params) collectParamSourceRefs(route.params, refs, modelsWithInput);
724
+ if (op.request) {
725
+ for (const body of op.request.bodies) collectTypeRefs(body.bodyType, refs, modelsWithInput, true);
726
+ }
727
+ for (const resp of op.responses) {
728
+ if (resp.bodyType) collectTypeRefs(resp.bodyType, refs, modelsWithInput, false);
729
+ }
730
+ if (op.query) collectParamSourceRefs(op.query, refs, modelsWithInput);
731
+ if (op.headers) collectParamSourceRefs(op.headers, refs, modelsWithInput);
732
+ }
733
+ }
734
+ return refs;
735
+ }
736
+ __name(collectReferencedModels, "collectReferencedModels");
737
+ function collectParamSourceRefs(source, out, modelsWithInput) {
738
+ if (source.kind === "ref") {
739
+ addModelRef(source.name, out, modelsWithInput);
740
+ } else if (source.kind === "params") {
741
+ for (const p of source.nodes) collectTypeRefs(p.type, out, modelsWithInput);
742
+ } else {
743
+ collectTypeRefs(source.node, out, modelsWithInput);
744
+ }
745
+ }
746
+ __name(collectParamSourceRefs, "collectParamSourceRefs");
747
+ function addModelRef(name, out, modelsWithInput) {
748
+ if (/^[A-Z]/.test(name)) {
749
+ out.add(name);
750
+ if (modelsWithInput?.has(name)) out.add(`${name}Input`);
751
+ }
752
+ }
753
+ __name(addModelRef, "addModelRef");
754
+ function collectTypeRefs(type, out, modelsWithInput, forInput = false) {
755
+ switch (type.kind) {
756
+ case "scalar":
757
+ switch (type.name) {
758
+ case "date":
759
+ out.add("__date__");
760
+ break;
761
+ case "time":
762
+ out.add("__time__");
763
+ break;
764
+ case "datetime":
765
+ out.add("__datetime__");
766
+ break;
767
+ case "uuid":
768
+ out.add("__uuid__");
769
+ break;
770
+ case "unknown":
771
+ case "json":
772
+ case "object":
773
+ out.add("__any__");
774
+ break;
775
+ }
776
+ break;
777
+ case "ref":
778
+ if (forInput && modelsWithInput?.has(type.name)) {
779
+ out.add(`${type.name}Input`);
780
+ } else {
781
+ addModelRef(type.name, out, modelsWithInput);
782
+ }
783
+ break;
784
+ case "array":
785
+ collectTypeRefs(type.item, out, modelsWithInput, forInput);
786
+ break;
787
+ case "tuple":
788
+ type.items.forEach((t) => collectTypeRefs(t, out, modelsWithInput, forInput));
789
+ break;
790
+ case "record":
791
+ collectTypeRefs(type.key, out, modelsWithInput, forInput);
792
+ collectTypeRefs(type.value, out, modelsWithInput, forInput);
793
+ break;
794
+ case "union":
795
+ type.members.forEach((m) => collectTypeRefs(m, out, modelsWithInput, forInput));
796
+ break;
797
+ case "discriminatedUnion":
798
+ type.members.forEach((m) => collectTypeRefs(m, out, modelsWithInput, forInput));
799
+ break;
800
+ case "intersection":
801
+ out.add("__any__");
802
+ break;
803
+ case "lazy":
804
+ collectTypeRefs(type.inner, out, modelsWithInput, forInput);
805
+ break;
806
+ case "inlineObject":
807
+ out.add("__any__");
808
+ break;
809
+ }
810
+ }
811
+ __name(collectTypeRefs, "collectTypeRefs");
812
+ function deriveBaseName(file) {
813
+ const base = file.split("/").pop()?.replace(/\.(op\.)?ck$/, "") ?? "Resource";
814
+ return base.split(".").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
815
+ }
816
+ __name(deriveBaseName, "deriveBaseName");
817
+ function deriveClientClassName(file) {
818
+ return `${deriveBaseName(file)}Client`;
819
+ }
820
+ __name(deriveClientClassName, "deriveClientClassName");
821
+ function deriveClientModuleName(file) {
822
+ const base = file.split("/").pop()?.replace(/\.(op\.)?ck$/, "") ?? "client";
823
+ const clean = base.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
824
+ return `_client_${clean}`;
825
+ }
826
+ __name(deriveClientModuleName, "deriveClientModuleName");
827
+ function deriveClientPropertyName(file) {
828
+ const base = deriveBaseName(file);
829
+ return base.charAt(0).toLowerCase() + base.slice(1);
830
+ }
831
+ __name(deriveClientPropertyName, "deriveClientPropertyName");
832
+ function deriveMethodName(op, route) {
833
+ if (op.sdk) return toSnakeCase(op.sdk);
834
+ if (op.name) return toSnakeCase(op.name);
835
+ return inferMethodName(op.method, route.path);
836
+ }
837
+ __name(deriveMethodName, "deriveMethodName");
838
+ function inferMethodName(method, path) {
839
+ const segments = path.split("/").filter((s) => s.length > 0);
840
+ const parts = [
841
+ method.toLowerCase()
842
+ ];
843
+ for (const seg of segments) {
844
+ if (seg.startsWith("{")) {
845
+ const paramName = seg.slice(1, -1);
846
+ parts.push("by_" + toSnakeCase(paramName));
847
+ } else {
848
+ parts.push(toSnakeCase(seg.replace(/[.-]/g, "_")));
849
+ }
850
+ }
851
+ return parts.join("_").replace(/_+/g, "_");
852
+ }
853
+ __name(inferMethodName, "inferMethodName");
854
+ function toSnakeCase(s) {
855
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]/g, "_").toLowerCase().replace(/_+/g, "_").replace(/^_|_$/g, "");
856
+ }
857
+ __name(toSnakeCase, "toSnakeCase");
858
+ var BASE_CLIENT_PY = `# Auto-generated by contractkit-plugin-python-sdk. Do not edit manually.
859
+ from __future__ import annotations
860
+
861
+ import httpx
862
+ from typing import Any
863
+
864
+
865
+ class SdkError(Exception):
866
+ def __init__(self, status: int, status_text: str, body: Any):
867
+ super().__init__(f"{status} {status_text}")
868
+ self.status = status
869
+ self.status_text = status_text
870
+ self.body = body
871
+
872
+
873
+ class BaseClient:
874
+ def __init__(self, base_url: str, headers: dict[str, str] | None = None):
875
+ self._base_url = base_url.rstrip("/")
876
+ self._headers = headers or {}
877
+ self._http = httpx.AsyncClient()
878
+
879
+ async def _fetch(
880
+ self,
881
+ path: str,
882
+ *,
883
+ method: str,
884
+ body: Any = None,
885
+ params: dict | None = None,
886
+ extra_headers: dict | None = None,
887
+ content_type: str | None = None,
888
+ body_kind: str = "json",
889
+ response_kind: str = "json",
890
+ ) -> Any:
891
+ result, _ = await self._fetch_with_headers(
892
+ path,
893
+ method=method,
894
+ body=body,
895
+ params=params,
896
+ extra_headers=extra_headers,
897
+ content_type=content_type,
898
+ body_kind=body_kind,
899
+ response_kind=response_kind,
900
+ )
901
+ return result
902
+
903
+ async def _fetch_with_headers(
904
+ self,
905
+ path: str,
906
+ *,
907
+ method: str,
908
+ body: Any = None,
909
+ params: dict | None = None,
910
+ extra_headers: dict | None = None,
911
+ content_type: str | None = None,
912
+ body_kind: str = "json",
913
+ response_kind: str = "json",
914
+ ) -> tuple[Any, dict[str, str]]:
915
+ headers = {**self._headers, **(extra_headers or {})}
916
+ if body is not None:
917
+ headers["Content-Type"] = content_type or "application/json"
918
+ # body_kind controls how httpx serializes the request body:
919
+ # "json" \u2014 body is a JSON-serializable object, sent via httpx's json= kwarg
920
+ # "text"/"binary" \u2014 body is a raw str/bytes payload, sent via content= unchanged
921
+ request_kwargs: dict[str, Any] = {"method": method, "url": f"{self._base_url}{path}", "params": params, "headers": headers}
922
+ if body is not None:
923
+ if body_kind == "json":
924
+ request_kwargs["json"] = body
925
+ else:
926
+ request_kwargs["content"] = body
927
+ response = await self._http.request(**request_kwargs)
928
+ if not response.is_success:
929
+ try:
930
+ error_body = response.json()
931
+ except Exception:
932
+ error_body = response.text
933
+ raise SdkError(response.status_code, response.reason_phrase, error_body)
934
+ # HTTP headers are case-insensitive \u2014 normalize to lowercase keys for stable lookup.
935
+ response_headers = {k.lower(): v for k, v in response.headers.items()}
936
+ if response.status_code == 204 or not response.content:
937
+ return None, response_headers
938
+ if response_kind == "text":
939
+ return response.text, response_headers
940
+ if response_kind == "binary":
941
+ return response.content, response_headers
942
+ return response.json(), response_headers
943
+ `;
944
+
945
+ // src/index.ts
946
+ var plugin = {
947
+ name: "python-sdk",
948
+ cacheKey: "python-sdk",
949
+ async generateTargets(inputs, ctx) {
950
+ const config = ctx.options;
951
+ return createPythonSdkPlugin(config, ctx.rootDir).generateTargets(inputs, ctx);
952
+ }
953
+ };
954
+ var index_default = plugin;
955
+ function createPythonSdkPlugin(config, rootDir) {
956
+ return {
957
+ name: "python-sdk",
958
+ cacheKey: `python-sdk:${JSON.stringify(config)}`,
959
+ async generateTargets({ contractRoots, opRoots, modelsWithInput: _modelsWithInput }, ctx) {
960
+ const modelsWithInput = _modelsWithInput;
961
+ const outDir = resolve(rootDir, config.baseDir ?? "python-sdk");
962
+ const modelModulePaths = /* @__PURE__ */ new Map();
963
+ const contractEntries = [];
964
+ for (const contractRoot of contractRoots) {
965
+ const moduleName = deriveModelsModuleName(contractRoot.file);
966
+ const outPath = join(outDir, `${moduleName}.py`);
967
+ contractEntries.push({
968
+ moduleName,
969
+ outPath,
970
+ root: contractRoot
971
+ });
972
+ for (const model of contractRoot.models) {
973
+ modelModulePaths.set(model.name, `.${moduleName}`);
974
+ if (modelsWithInput.has(model.name)) {
975
+ modelModulePaths.set(`${model.name}Input`, `.${moduleName}`);
976
+ }
977
+ }
978
+ }
979
+ for (const { moduleName, outPath, root } of contractEntries) {
980
+ const content = generatePydanticModels(root, {
981
+ modelModulePaths,
982
+ currentModule: `.${moduleName}`,
983
+ modelsWithInput
984
+ });
985
+ ctx.emitFile(outPath, content);
986
+ }
987
+ const clientInfos = [];
988
+ for (const opRoot of opRoots) {
989
+ if (!hasPublicOperations(opRoot, config.includeInternal)) continue;
990
+ const moduleName = deriveClientModuleName(opRoot.file);
991
+ const outPath = join(outDir, `${moduleName}.py`);
992
+ clientInfos.push({
993
+ moduleName,
994
+ className: deriveClientClassName(opRoot.file),
995
+ propertyName: deriveClientPropertyName(opRoot.file)
996
+ });
997
+ ctx.emitFile(outPath, generatePythonClient(opRoot, {
998
+ modelModulePaths,
999
+ currentModule: `.${moduleName}`,
1000
+ modelsWithInput,
1001
+ includeInternal: config.includeInternal
1002
+ }));
1003
+ }
1004
+ ctx.emitFile(join(outDir, "_base_client.py"), BASE_CLIENT_PY);
1005
+ ctx.emitFile(join(outDir, "requirements.txt"), "httpx\npydantic>=2.0\n");
1006
+ const sdkClassName = config.packageName ? config.packageName.split(/[-._\s]+/).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Sdk" : "Sdk";
1007
+ const initLines = [
1008
+ "# Auto-generated by contractkit-plugin-python-sdk. Do not edit manually.",
1009
+ "from ._base_client import BaseClient, SdkError"
1010
+ ];
1011
+ for (const c of clientInfos) {
1012
+ initLines.push(`from .${c.moduleName} import ${c.className}`);
1013
+ }
1014
+ initLines.push("");
1015
+ initLines.push("");
1016
+ if (clientInfos.length > 0) {
1017
+ initLines.push(`class ${sdkClassName}(BaseClient):`);
1018
+ initLines.push(` def __init__(self, base_url: str, headers: dict[str, str] | None = None):`);
1019
+ initLines.push(` super().__init__(base_url, headers)`);
1020
+ for (const c of clientInfos) {
1021
+ initLines.push(` self.${c.propertyName} = ${c.className}(base_url, headers)`);
1022
+ }
1023
+ initLines.push("");
1024
+ } else {
1025
+ initLines.push(`class ${sdkClassName}(BaseClient):`);
1026
+ initLines.push(` pass`);
1027
+ initLines.push("");
1028
+ }
1029
+ const allNames = [
1030
+ "BaseClient",
1031
+ "SdkError",
1032
+ sdkClassName,
1033
+ ...clientInfos.map((c) => c.className)
1034
+ ];
1035
+ initLines.push(`__all__ = [${allNames.map((n) => JSON.stringify(n)).join(", ")}]`);
1036
+ initLines.push("");
1037
+ ctx.emitFile(join(outDir, "__init__.py"), initLines.join("\n"));
1038
+ }
1039
+ };
1040
+ }
1041
+ __name(createPythonSdkPlugin, "createPythonSdkPlugin");
1042
+ export {
1043
+ createPythonSdkPlugin,
1044
+ index_default as default
1045
+ };
1046
+ //# sourceMappingURL=index.js.map