chrometools-mcp 3.3.8 → 3.3.9

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.
@@ -0,0 +1,448 @@
1
+ /**
2
+ * utils/api-generators/api-models-python.js
3
+ *
4
+ * Generates Python dataclass/pydantic/typeddict models from OpenAPI schemas.
5
+ */
6
+
7
+ import { TypeMapper } from '../openapi/type-mapper.js';
8
+ import { mergeAllOf } from '../openapi/ref-resolver.js';
9
+ import { arraysEqual, schemaSignature } from '../openapi/helpers.js';
10
+
11
+ export class ApiModelsPythonGenerator {
12
+ constructor(schemas, options = {}) {
13
+ this.schemas = schemas;
14
+ this.options = {
15
+ style: 'dataclass', // 'dataclass' | 'pydantic' | 'typeddict'
16
+ includeEnums: true,
17
+ includeValidation: false,
18
+ ...options
19
+ };
20
+ this.generatedEnums = new Set();
21
+ this.usesDatetime = false;
22
+ this.usesDate = false;
23
+ }
24
+
25
+ /**
26
+ * Generate full file content
27
+ * @param {Object} metadata - { title, source, version }
28
+ * @returns {string}
29
+ */
30
+ generate(metadata = {}) {
31
+ // Pre-scan to detect datetime usage
32
+ this._scanForDateTypes();
33
+
34
+ const lines = [];
35
+
36
+ // Header
37
+ lines.push(...this.generateHeader(metadata));
38
+
39
+ // Imports
40
+ lines.push(...this.generateImports());
41
+ lines.push('');
42
+
43
+ // Enums first
44
+ if (this.options.includeEnums) {
45
+ const enumLines = this.generateAllEnums();
46
+ if (enumLines.length > 0) {
47
+ lines.push(...enumLines);
48
+ }
49
+ }
50
+
51
+ // Models in dependency order
52
+ const sorted = this.topologicalSort();
53
+ for (const name of sorted) {
54
+ const schema = this.schemas[name];
55
+ if (!schema) continue;
56
+
57
+ if (schema.enum && schema.type === 'string' && this.generatedEnums.has(name)) continue;
58
+
59
+ const modelLines = this.generateModel(name, schema);
60
+ if (modelLines.length > 0) {
61
+ lines.push('');
62
+ lines.push(...modelLines);
63
+ }
64
+ }
65
+
66
+ return lines.join('\n') + '\n';
67
+ }
68
+
69
+ generateHeader(metadata) {
70
+ return [
71
+ '"""',
72
+ `Generated from ${metadata.title || 'OpenAPI'} (${metadata.source || 'unknown source'})`,
73
+ `OpenAPI ${metadata.version || 'unknown'} | Generated at ${new Date().toISOString()}`,
74
+ '"""',
75
+ ''
76
+ ];
77
+ }
78
+
79
+ generateImports() {
80
+ const lines = ['from __future__ import annotations'];
81
+
82
+ if (this.options.style === 'dataclass') {
83
+ lines.push('from dataclasses import dataclass, field');
84
+ lines.push('from enum import Enum');
85
+ lines.push('from typing import Optional, List, Dict, Any, Union');
86
+ } else if (this.options.style === 'pydantic') {
87
+ lines.push('from pydantic import BaseModel, Field');
88
+ lines.push('from enum import Enum');
89
+ lines.push('from typing import Optional, List, Dict, Any, Union');
90
+ } else {
91
+ // typeddict
92
+ lines.push('from typing import TypedDict, Optional, List, Dict, Any, Union');
93
+ lines.push('from enum import Enum');
94
+ }
95
+
96
+ if (this.usesDatetime || this.usesDate) {
97
+ const imports = [];
98
+ if (this.usesDatetime) imports.push('datetime');
99
+ if (this.usesDate) imports.push('date');
100
+ lines.push(`from datetime import ${imports.join(', ')}`);
101
+ }
102
+
103
+ return lines;
104
+ }
105
+
106
+ _scanForDateTypes() {
107
+ const scan = (obj) => {
108
+ if (!obj || typeof obj !== 'object') return;
109
+ if (obj.format === 'date-time') this.usesDatetime = true;
110
+ if (obj.format === 'date') this.usesDate = true;
111
+ for (const val of Object.values(obj)) {
112
+ if (val && typeof val === 'object') scan(val);
113
+ }
114
+ };
115
+ scan(this.schemas);
116
+ }
117
+
118
+ generateAllEnums() {
119
+ const lines = [];
120
+ for (const [name, schema] of Object.entries(this.schemas)) {
121
+ if (schema.enum && schema.type === 'string') {
122
+ lines.push('');
123
+ lines.push(...this.generateEnum(name, schema.enum, schema.description));
124
+ this.generatedEnums.add(name);
125
+ continue;
126
+ }
127
+ if (schema.properties) {
128
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
129
+ if (propSchema.enum && propSchema.type === 'string') {
130
+ // Skip if values match a top-level enum
131
+ const matchingEnum = this._findMatchingEnum(propSchema.enum);
132
+ if (matchingEnum) continue;
133
+
134
+ const enumName = name + propName.charAt(0).toUpperCase() + propName.slice(1);
135
+ if (!this.generatedEnums.has(enumName)) {
136
+ lines.push('');
137
+ lines.push(...this.generateEnum(enumName, propSchema.enum, propSchema.description));
138
+ this.generatedEnums.add(enumName);
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ return lines;
145
+ }
146
+
147
+ generateEnum(name, values, description) {
148
+ const lines = [];
149
+ lines.push(`class ${name}(str, Enum):`);
150
+ if (description) {
151
+ lines.push(` """${description}"""`);
152
+ }
153
+ for (const value of values) {
154
+ let key = value.toUpperCase().replace(/[^A-Z0-9]/g, '_');
155
+ if (/^\d/.test(key)) key = `_${key}`;
156
+ lines.push(` ${key} = '${value}'`);
157
+ }
158
+ return lines;
159
+ }
160
+
161
+ generateModel(name, schema) {
162
+ if (schema.allOf) {
163
+ return this.generateAllOfModel(name, schema);
164
+ }
165
+
166
+ if (schema.oneOf || schema.anyOf) {
167
+ return this.generateUnionType(name, schema.oneOf || schema.anyOf, schema.description);
168
+ }
169
+
170
+ if (schema.type === 'object' || schema.properties) {
171
+ return this.generateObjectModel(name, schema);
172
+ }
173
+
174
+ // Simple type alias
175
+ if (schema.type && schema.type !== 'object') {
176
+ const pyType = TypeMapper.toPython(schema);
177
+ return [`${name} = ${pyType}`];
178
+ }
179
+
180
+ return this.generateObjectModel(name, { type: 'object', properties: {} });
181
+ }
182
+
183
+ generateObjectModel(name, schema) {
184
+ const lines = [];
185
+ const required = new Set(schema.required || []);
186
+
187
+ if (this.options.style === 'dataclass') {
188
+ lines.push('@dataclass');
189
+ lines.push(`class ${name}:`);
190
+ } else if (this.options.style === 'pydantic') {
191
+ lines.push(`class ${name}(BaseModel):`);
192
+ } else {
193
+ lines.push(`class ${name}(TypedDict, total=False):`);
194
+ }
195
+
196
+ if (schema.description) {
197
+ lines.push(` """${schema.description}"""`);
198
+ }
199
+
200
+ const props = Object.entries(schema.properties || {});
201
+
202
+ if (props.length === 0) {
203
+ lines.push(' pass');
204
+ return lines;
205
+ }
206
+
207
+ // Sort: required first, then optional
208
+ const sortedProps = props.sort(([a], [b]) => {
209
+ const aReq = required.has(a);
210
+ const bReq = required.has(b);
211
+ if (aReq && !bReq) return -1;
212
+ if (!aReq && bReq) return 1;
213
+ return 0;
214
+ });
215
+
216
+ for (const [propName, propSchema] of sortedProps) {
217
+ const isRequired = required.has(propName);
218
+ const pyName = this.toSnakeCase(propName);
219
+ const type = this.resolvePropertyType(name, propName, propSchema);
220
+
221
+ if (this.options.style === 'typeddict') {
222
+ if (isRequired) {
223
+ lines.push(` ${pyName}: ${type}`);
224
+ } else {
225
+ lines.push(` ${pyName}: Optional[${type}]`);
226
+ }
227
+ } else if (this.options.style === 'pydantic') {
228
+ if (isRequired) {
229
+ lines.push(` ${pyName}: ${type}`);
230
+ } else {
231
+ lines.push(` ${pyName}: Optional[${type}] = None`);
232
+ }
233
+ } else {
234
+ // dataclass: required fields have no default (enforced at construction)
235
+ if (isRequired) {
236
+ lines.push(` ${pyName}: ${type}`);
237
+ } else {
238
+ lines.push(` ${pyName}: Optional[${type}] = None`);
239
+ }
240
+ }
241
+ }
242
+
243
+ return lines;
244
+ }
245
+
246
+ generateAllOfModel(name, schema) {
247
+ const lines = [];
248
+ const namedRefs = [];
249
+ const inlineSchemas = [];
250
+
251
+ for (const sub of schema.allOf) {
252
+ const refName = this._findSchemaName(sub);
253
+ if (refName) {
254
+ namedRefs.push(refName);
255
+ } else {
256
+ inlineSchemas.push(sub);
257
+ }
258
+ }
259
+
260
+ const merged = mergeAllOf(inlineSchemas.length > 0 ? inlineSchemas : schema.allOf);
261
+ const baseClass = namedRefs.length > 0 ? namedRefs[0] : null;
262
+ const required = new Set(merged.required || []);
263
+
264
+ if (this.options.style === 'dataclass') {
265
+ lines.push('@dataclass');
266
+ if (baseClass) {
267
+ lines.push(`class ${name}(${baseClass}):`);
268
+ } else {
269
+ lines.push(`class ${name}:`);
270
+ }
271
+ } else if (this.options.style === 'pydantic') {
272
+ if (baseClass) {
273
+ lines.push(`class ${name}(${baseClass}):`);
274
+ } else {
275
+ lines.push(`class ${name}(BaseModel):`);
276
+ }
277
+ } else {
278
+ if (baseClass) {
279
+ lines.push(`class ${name}(${baseClass}, total=False):`);
280
+ } else {
281
+ lines.push(`class ${name}(TypedDict, total=False):`);
282
+ }
283
+ }
284
+
285
+ if (schema.description) {
286
+ lines.push(` """${schema.description}"""`);
287
+ }
288
+
289
+ const props = Object.entries(merged.properties || {});
290
+ if (props.length === 0 && !baseClass) {
291
+ lines.push(' pass');
292
+ return lines;
293
+ }
294
+
295
+ if (props.length === 0) {
296
+ lines.push(' pass');
297
+ return lines;
298
+ }
299
+
300
+ for (const [propName, propSchema] of props) {
301
+ const isRequired = required.has(propName);
302
+ const pyName = this.toSnakeCase(propName);
303
+ const type = this.resolvePropertyType(name, propName, propSchema);
304
+
305
+ if (this.options.style === 'pydantic') {
306
+ lines.push(isRequired
307
+ ? ` ${pyName}: ${type}`
308
+ : ` ${pyName}: Optional[${type}] = None`);
309
+ } else if (this.options.style === 'dataclass') {
310
+ lines.push(isRequired
311
+ ? ` ${pyName}: ${type}`
312
+ : ` ${pyName}: Optional[${type}] = None`);
313
+ } else {
314
+ lines.push(isRequired
315
+ ? ` ${pyName}: ${type}`
316
+ : ` ${pyName}: Optional[${type}]`);
317
+ }
318
+ }
319
+
320
+ return lines;
321
+ }
322
+
323
+ generateUnionType(name, schemas, description) {
324
+ const lines = [];
325
+ if (description) {
326
+ lines.push(`# ${description}`);
327
+ }
328
+ const types = schemas.map(s => {
329
+ const refName = this._findSchemaName(s);
330
+ if (refName) return refName;
331
+ return TypeMapper.toPython(s);
332
+ });
333
+ lines.push(`${name} = Union[${types.join(', ')}]`);
334
+ return lines;
335
+ }
336
+
337
+ resolvePropertyType(parentName, propName, propSchema) {
338
+ // Enum property
339
+ if (propSchema.enum && propSchema.type === 'string' && this.options.includeEnums) {
340
+ for (const [schemaName, schema] of Object.entries(this.schemas)) {
341
+ if (schema.enum && schema.type === 'string' && arraysEqual(schema.enum, propSchema.enum)) {
342
+ return schemaName;
343
+ }
344
+ }
345
+ return parentName + propName.charAt(0).toUpperCase() + propName.slice(1);
346
+ }
347
+
348
+ // Array with known items
349
+ if (propSchema.type === 'array' && propSchema.items) {
350
+ const itemName = this._findSchemaName(propSchema.items);
351
+ if (itemName) return `List[${itemName}]`;
352
+ }
353
+
354
+ // Object reference
355
+ const refName = this._findSchemaName(propSchema);
356
+ if (refName) return refName;
357
+
358
+ return TypeMapper.toPython(propSchema);
359
+ }
360
+
361
+ getDefaultValue(type, schema) {
362
+ if (type === 'str') return "''";
363
+ if (type === 'int') return '0';
364
+ if (type === 'float') return '0.0';
365
+ if (type === 'bool') return 'False';
366
+ if (type.startsWith('List[')) return 'field(default_factory=list)';
367
+ if (type.startsWith('Dict[')) return 'field(default_factory=dict)';
368
+ return 'None';
369
+ }
370
+
371
+ toSnakeCase(name) {
372
+ return name.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '').replace(/-/g, '_');
373
+ }
374
+
375
+ _findMatchingEnum(enumValues) {
376
+ for (const [name, schema] of Object.entries(this.schemas)) {
377
+ if (schema.enum && schema.type === 'string' && arraysEqual(schema.enum, enumValues)) {
378
+ return name;
379
+ }
380
+ }
381
+ return null;
382
+ }
383
+
384
+ _findSchemaName(schema) {
385
+ if (!schema || typeof schema !== 'object') return null;
386
+ if (schema.$circularRef) return `'${schema.$circularRef}'`;
387
+ if (schema._refName && this.schemas[schema._refName]) return schema._refName;
388
+
389
+ for (const [name, s] of Object.entries(this.schemas)) {
390
+ if (s === schema) return name;
391
+ if (s.properties && schema.properties) {
392
+ const sSig = schemaSignature(s.properties);
393
+ const schemaSig = schemaSignature(schema.properties);
394
+ if (sSig === schemaSig && sSig.length > 0) return name;
395
+ }
396
+ }
397
+ return null;
398
+ }
399
+
400
+ topologicalSort() {
401
+ const names = Object.keys(this.schemas);
402
+ const visited = new Set();
403
+ const result = [];
404
+
405
+ const visit = (name) => {
406
+ if (visited.has(name)) return;
407
+ visited.add(name);
408
+ const deps = this.getDependencies(name);
409
+ for (const dep of deps) {
410
+ if (this.schemas[dep]) visit(dep);
411
+ }
412
+ result.push(name);
413
+ };
414
+
415
+ for (const name of names) {
416
+ visit(name);
417
+ }
418
+ return result;
419
+ }
420
+
421
+ getDependencies(name) {
422
+ const schema = this.schemas[name];
423
+ if (!schema) return [];
424
+ const deps = new Set();
425
+ const seen = new WeakSet();
426
+
427
+ const collectDeps = (obj) => {
428
+ if (!obj || typeof obj !== 'object') return;
429
+ if (obj.$circularRef) { deps.add(obj.$circularRef); return; }
430
+ if (seen.has(obj)) return;
431
+ seen.add(obj);
432
+ if (Array.isArray(obj)) { obj.forEach(collectDeps); return; }
433
+ const refName = this._findSchemaName(obj);
434
+ if (refName && refName !== name && !refName.startsWith("'")) deps.add(refName);
435
+ for (const val of Object.values(obj)) {
436
+ if (val && typeof val === 'object') collectDeps(val);
437
+ }
438
+ };
439
+
440
+ if (schema.properties) collectDeps(schema.properties);
441
+ if (schema.allOf) collectDeps(schema.allOf);
442
+ if (schema.oneOf) collectDeps(schema.oneOf);
443
+ if (schema.anyOf) collectDeps(schema.anyOf);
444
+ if (schema.items) collectDeps(schema.items);
445
+
446
+ return [...deps];
447
+ }
448
+ }