@archrad/deterministic 0.1.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/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +15 -0
- package/LICENSE +17 -0
- package/README.md +284 -0
- package/SECURITY.md +26 -0
- package/biome.json +25 -0
- package/demo-validate.gif +0 -0
- package/dist/cli-findings.d.ts +23 -0
- package/dist/cli-findings.d.ts.map +1 -0
- package/dist/cli-findings.js +88 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +341 -0
- package/dist/edgeConfigCodeGenerator.d.ts +55 -0
- package/dist/edgeConfigCodeGenerator.d.ts.map +1 -0
- package/dist/edgeConfigCodeGenerator.js +249 -0
- package/dist/exportPipeline.d.ts +23 -0
- package/dist/exportPipeline.d.ts.map +1 -0
- package/dist/exportPipeline.js +65 -0
- package/dist/golden-bundle.d.ts +21 -0
- package/dist/golden-bundle.d.ts.map +1 -0
- package/dist/golden-bundle.js +166 -0
- package/dist/graphPredicates.d.ts +10 -0
- package/dist/graphPredicates.d.ts.map +1 -0
- package/dist/graphPredicates.js +33 -0
- package/dist/hostPort.d.ts +12 -0
- package/dist/hostPort.d.ts.map +1 -0
- package/dist/hostPort.js +39 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/ir-lint.d.ts +11 -0
- package/dist/ir-lint.d.ts.map +1 -0
- package/dist/ir-lint.js +16 -0
- package/dist/ir-normalize.d.ts +48 -0
- package/dist/ir-normalize.d.ts.map +1 -0
- package/dist/ir-normalize.js +81 -0
- package/dist/ir-structural.d.ts +40 -0
- package/dist/ir-structural.d.ts.map +1 -0
- package/dist/ir-structural.js +267 -0
- package/dist/lint-graph.d.ts +40 -0
- package/dist/lint-graph.d.ts.map +1 -0
- package/dist/lint-graph.js +133 -0
- package/dist/lint-rules.d.ts +40 -0
- package/dist/lint-rules.d.ts.map +1 -0
- package/dist/lint-rules.js +290 -0
- package/dist/nodeExpress.d.ts +2 -0
- package/dist/nodeExpress.d.ts.map +1 -0
- package/dist/nodeExpress.js +528 -0
- package/dist/openapi-structural.d.ts +26 -0
- package/dist/openapi-structural.d.ts.map +1 -0
- package/dist/openapi-structural.js +82 -0
- package/dist/openapi-to-ir.d.ts +26 -0
- package/dist/openapi-to-ir.d.ts.map +1 -0
- package/dist/openapi-to-ir.js +131 -0
- package/dist/pythonFastAPI.d.ts +2 -0
- package/dist/pythonFastAPI.d.ts.map +1 -0
- package/dist/pythonFastAPI.js +664 -0
- package/dist/validate-drift.d.ts +54 -0
- package/dist/validate-drift.d.ts.map +1 -0
- package/dist/validate-drift.js +184 -0
- package/dist/yamlToIr.d.ts +14 -0
- package/dist/yamlToIr.d.ts.map +1 -0
- package/dist/yamlToIr.js +39 -0
- package/docs/CONCEPT_ADOPTION_AND_LIMITS.md +47 -0
- package/docs/CUSTOM_RULES.md +87 -0
- package/docs/ENGINEERING_NOTES.md +42 -0
- package/docs/IR_CONTRACT.md +54 -0
- package/docs/STRUCTURAL_VS_SEMANTIC_VALIDATION.md +86 -0
- package/fixtures/demo-direct-db-layered.json +37 -0
- package/fixtures/demo-direct-db-violation.json +22 -0
- package/fixtures/ecommerce-with-warnings.json +89 -0
- package/fixtures/invalid-cycle.json +15 -0
- package/fixtures/invalid-edge-unknown-node.json +14 -0
- package/fixtures/minimal-graph.json +14 -0
- package/fixtures/minimal-graph.yaml +13 -0
- package/fixtures/payment-retry-demo.json +43 -0
- package/llms.txt +99 -0
- package/package.json +84 -0
- package/schemas/archrad-ir-graph-v1.schema.json +67 -0
- package/scripts/DEMO_GIF_STORYBOARD.md +100 -0
- package/scripts/GIF_RECORDING_STEP_BY_STEP.md +125 -0
- package/scripts/README_DEMO_RECORDING.md +314 -0
- package/scripts/SOCIAL_POST_DRIFT_AND_INGESTION.md +17 -0
- package/scripts/golden-path-demo.ps1 +25 -0
- package/scripts/golden-path-demo.sh +23 -0
- package/scripts/invoke-drift-check.ps1 +16 -0
- package/scripts/record-demo-drift.tape +50 -0
- package/scripts/record-demo-payment-retry.tape +36 -0
- package/scripts/record-demo-validate.tape +34 -0
- package/scripts/record-demo.tape +33 -0
- package/scripts/run-demo-drift-sequence.ps1 +45 -0
- package/scripts/run-demo-drift-sequence.sh +41 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
// Deterministic Python FastAPI exporter
|
|
2
|
+
// Produces a map of filename -> content given an IR (plan graph) and options.
|
|
3
|
+
import { getEdgeConfig, generateRetryCode, generateCircuitBreakerCode } from './edgeConfigCodeGenerator.js';
|
|
4
|
+
function safeId(id) {
|
|
5
|
+
return String(id || '').replace(/[^A-Za-z0-9_\-]/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'node';
|
|
6
|
+
}
|
|
7
|
+
function handlerNameFor(n) {
|
|
8
|
+
if (n && n.config && n.config.name)
|
|
9
|
+
return String(n.config.name).replace(/[^A-Za-z0-9_]/g, '_');
|
|
10
|
+
const id = safeId(n && n.id ? n.id : n && n.name ? n.name : 'handler');
|
|
11
|
+
return `handler_${id.replace(/-/g, '_')}`;
|
|
12
|
+
}
|
|
13
|
+
function pyTypeFromSchema(def) {
|
|
14
|
+
const dtype = def?.type || def?.dataType || (def?.properties ? 'object' : def?.items ? 'array' : 'string');
|
|
15
|
+
if (dtype === 'number' || dtype === 'integer')
|
|
16
|
+
return 'float';
|
|
17
|
+
if (dtype === 'boolean')
|
|
18
|
+
return 'bool';
|
|
19
|
+
if (dtype === 'array')
|
|
20
|
+
return 'List[Any]';
|
|
21
|
+
if (dtype === 'object')
|
|
22
|
+
return 'Dict[str, Any]';
|
|
23
|
+
return 'str';
|
|
24
|
+
}
|
|
25
|
+
function buildPydanticModel(className, schema) {
|
|
26
|
+
if (!schema || typeof schema !== 'object') {
|
|
27
|
+
return `class ${className}(BaseModel):\n status: str = "ok"\n message: str | None = None\n data: Dict[str, Any] | None = None\n`;
|
|
28
|
+
}
|
|
29
|
+
const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : schema;
|
|
30
|
+
const lines = [`class ${className}(BaseModel):`];
|
|
31
|
+
lines.push(' status: str = "ok"');
|
|
32
|
+
lines.push(' message: str | None = None');
|
|
33
|
+
lines.push(' data: Dict[str, Any] | None = None');
|
|
34
|
+
if (props && typeof props === 'object') {
|
|
35
|
+
const keys = Object.keys(props);
|
|
36
|
+
keys.forEach((k) => {
|
|
37
|
+
const ident = k.replace(/[^A-Za-z0-9_]/g, '_') || 'field';
|
|
38
|
+
const t = pyTypeFromSchema(props[k]);
|
|
39
|
+
lines.push(` ${ident}: ${t} | None = None`);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
lines.push('');
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
function renderSchema(schema, indent = ' ') {
|
|
46
|
+
if (!schema || typeof schema !== 'object') {
|
|
47
|
+
return [`${indent}type: object`];
|
|
48
|
+
}
|
|
49
|
+
const lines = [];
|
|
50
|
+
const t = schema.type ||
|
|
51
|
+
schema.dataType ||
|
|
52
|
+
(schema.properties ? 'object' : Array.isArray(schema) || schema.items ? 'array' : 'object');
|
|
53
|
+
lines.push(`${indent}type: ${t === 'integer' ? 'number' : t}`);
|
|
54
|
+
if (t === 'array' && schema.items) {
|
|
55
|
+
lines.push(`${indent}items:`);
|
|
56
|
+
lines.push(...renderSchema(schema.items, `${indent} `));
|
|
57
|
+
}
|
|
58
|
+
if (t === 'object' && schema.properties && typeof schema.properties === 'object') {
|
|
59
|
+
lines.push(`${indent}properties:`);
|
|
60
|
+
Object.entries(schema.properties).forEach(([k, v]) => {
|
|
61
|
+
lines.push(`${indent} ${k}:`);
|
|
62
|
+
lines.push(...renderSchema(v, `${indent} `));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (schema.format) {
|
|
66
|
+
lines.push(`${indent}format: ${schema.format}`);
|
|
67
|
+
}
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
70
|
+
function buildOpenApiSpec(serviceName, endpoints, nonHttpNodes) {
|
|
71
|
+
const schemaEntries = [];
|
|
72
|
+
const seenSchemas = new Set();
|
|
73
|
+
const safeName = (route, method, kind) => `${method.toLowerCase()}_${route.replace(/[^A-Za-z0-9]+/g, '_') || 'root'}_${kind}`;
|
|
74
|
+
const addSchema = (name, schema) => {
|
|
75
|
+
const key = JSON.stringify(schema || {});
|
|
76
|
+
if (!schema || typeof schema !== 'object')
|
|
77
|
+
return null;
|
|
78
|
+
const compositeKey = `${name}:${key}`;
|
|
79
|
+
if (seenSchemas.has(compositeKey))
|
|
80
|
+
return name;
|
|
81
|
+
seenSchemas.add(compositeKey);
|
|
82
|
+
schemaEntries.push({ name, schema });
|
|
83
|
+
return name;
|
|
84
|
+
};
|
|
85
|
+
const lines = [];
|
|
86
|
+
lines.push('openapi: 3.0.0');
|
|
87
|
+
lines.push('info:');
|
|
88
|
+
lines.push(` title: ${serviceName}`);
|
|
89
|
+
lines.push(' version: 0.0.1');
|
|
90
|
+
lines.push('paths:');
|
|
91
|
+
if (!endpoints.length) {
|
|
92
|
+
lines.push(' /:');
|
|
93
|
+
lines.push(' get:');
|
|
94
|
+
lines.push(' responses:');
|
|
95
|
+
lines.push(' "200":');
|
|
96
|
+
lines.push(' description: OK');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
endpoints.forEach((ep) => {
|
|
100
|
+
const lower = ep.method.toLowerCase();
|
|
101
|
+
const success = String(ep.success || 200);
|
|
102
|
+
lines.push(` ${ep.route}:`);
|
|
103
|
+
lines.push(` ${lower}:`);
|
|
104
|
+
const reqRef = ep.requestSchema ? addSchema(safeName(ep.route, ep.method, 'request'), ep.requestSchema) : null;
|
|
105
|
+
const resRef = ep.responseSchema ? addSchema(safeName(ep.route, ep.method, 'response'), ep.responseSchema) : null;
|
|
106
|
+
if (ep.hasBody && success !== '204') {
|
|
107
|
+
lines.push(' requestBody:');
|
|
108
|
+
lines.push(' content:');
|
|
109
|
+
lines.push(' application/json:');
|
|
110
|
+
lines.push(' schema:');
|
|
111
|
+
if (reqRef) {
|
|
112
|
+
lines.push(` $ref: "#/components/schemas/${reqRef}"`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
lines.push(...renderSchema(ep.requestSchema || {}, ' '));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
lines.push(' responses:');
|
|
119
|
+
lines.push(` "${success}":`);
|
|
120
|
+
lines.push(` description: ${success === '201' ? 'Created' : success === '202' ? 'Accepted' : success === '204' ? 'No Content' : 'OK'}`);
|
|
121
|
+
if (success !== '204') {
|
|
122
|
+
lines.push(' content:');
|
|
123
|
+
lines.push(' application/json:');
|
|
124
|
+
lines.push(' schema:');
|
|
125
|
+
if (resRef) {
|
|
126
|
+
lines.push(` $ref: "#/components/schemas/${resRef}"`);
|
|
127
|
+
}
|
|
128
|
+
else if (ep.responseSchema) {
|
|
129
|
+
lines.push(...renderSchema(ep.responseSchema, ' '));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
lines.push(' $ref: "#/components/schemas/Response"');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
lines.push(' "400":');
|
|
136
|
+
lines.push(' description: Bad Request');
|
|
137
|
+
lines.push(' "401":');
|
|
138
|
+
lines.push(' description: Unauthorized');
|
|
139
|
+
lines.push(' "500":');
|
|
140
|
+
lines.push(' description: Server Error');
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
lines.push('components:');
|
|
144
|
+
lines.push(' schemas:');
|
|
145
|
+
lines.push(' Response:');
|
|
146
|
+
lines.push(' type: object');
|
|
147
|
+
lines.push(' properties:');
|
|
148
|
+
lines.push(' status:');
|
|
149
|
+
lines.push(' type: string');
|
|
150
|
+
lines.push(' message:');
|
|
151
|
+
lines.push(' type: string');
|
|
152
|
+
lines.push(' data:');
|
|
153
|
+
lines.push(' type: object');
|
|
154
|
+
schemaEntries.forEach((entry) => {
|
|
155
|
+
lines.push(` ${entry.name}:`);
|
|
156
|
+
lines.push(...renderSchema(entry.schema, ' '));
|
|
157
|
+
});
|
|
158
|
+
if (nonHttpNodes && nonHttpNodes.length) {
|
|
159
|
+
lines.push('x-nonHttpNodes:');
|
|
160
|
+
nonHttpNodes.forEach((n) => {
|
|
161
|
+
const componentName = addSchema(`nonhttp_${n.type}_${n.id}`, n.schema || {});
|
|
162
|
+
lines.push(` - id: ${n.id}`);
|
|
163
|
+
lines.push(` type: ${n.type}`);
|
|
164
|
+
lines.push(` name: ${JSON.stringify(n.name)}`);
|
|
165
|
+
if (n.config && Object.keys(n.config).length) {
|
|
166
|
+
lines.push(' configKeys:');
|
|
167
|
+
Object.keys(n.config).slice(0, 8).forEach((k) => lines.push(` - ${k}`));
|
|
168
|
+
}
|
|
169
|
+
if (componentName) {
|
|
170
|
+
lines.push(` schemaRef: "#/components/schemas/${componentName}"`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Generate code for inner nodes (support nodes) that are embedded within a key node
|
|
178
|
+
*/
|
|
179
|
+
function generateInnerNodeCode(innerNodes) {
|
|
180
|
+
const code = [];
|
|
181
|
+
for (const innerNode of innerNodes) {
|
|
182
|
+
if (!innerNode || !innerNode.type)
|
|
183
|
+
continue;
|
|
184
|
+
const innerType = String(innerNode.type || innerNode.kind || '').toLowerCase();
|
|
185
|
+
const innerId = safeId(innerNode.id || innerNode.name);
|
|
186
|
+
const innerCfg = (innerNode && innerNode.config) || {};
|
|
187
|
+
// Generate code based on inner node type
|
|
188
|
+
if (innerType === 'transform' && innerCfg.transform === 'authenticate') {
|
|
189
|
+
code.push(` # Inner node: Authentication (${innerId})`);
|
|
190
|
+
code.push(` auth_result = authenticate_request(request)`);
|
|
191
|
+
code.push(` if not auth_result.get('valid', False):`);
|
|
192
|
+
code.push(` return {"error": "Unauthorized", "status": 401}`);
|
|
193
|
+
code.push('');
|
|
194
|
+
}
|
|
195
|
+
else if (innerType === 'transform' && innerCfg.transform === 'validate') {
|
|
196
|
+
code.push(` # Inner node: Validation (${innerId})`);
|
|
197
|
+
code.push(` validation_result = validate_schema(payload)`);
|
|
198
|
+
code.push(` if not validation_result.get('valid', False):`);
|
|
199
|
+
code.push(` return {"error": "Validation failed", "status": 400, "details": validation_result.get('errors', [])}`);
|
|
200
|
+
code.push('');
|
|
201
|
+
}
|
|
202
|
+
else if (innerType === 'transform' && innerCfg.transform === 'parse') {
|
|
203
|
+
code.push(` # Inner node: Parse (${innerId})`);
|
|
204
|
+
code.push(` parsed_data = parse_payload(payload)`);
|
|
205
|
+
code.push('');
|
|
206
|
+
}
|
|
207
|
+
else if (innerType === 'retry' || innerCfg.retryPolicy) {
|
|
208
|
+
code.push(` # Inner node: Retry Logic (${innerId})`);
|
|
209
|
+
code.push(` max_attempts = ${innerCfg.maxAttempts || innerCfg.max_attempts || 3}`);
|
|
210
|
+
code.push(` for attempt in range(max_attempts):`);
|
|
211
|
+
code.push(` try:`);
|
|
212
|
+
code.push(` # Retryable operation`);
|
|
213
|
+
code.push(` break`);
|
|
214
|
+
code.push(` except Exception as e:`);
|
|
215
|
+
code.push(` if attempt == max_attempts - 1:`);
|
|
216
|
+
code.push(` raise`);
|
|
217
|
+
code.push(` await asyncio.sleep(2 ** attempt) # Exponential backoff`);
|
|
218
|
+
code.push('');
|
|
219
|
+
}
|
|
220
|
+
else if (innerType === 'error-handler' || innerCfg.errorHandling) {
|
|
221
|
+
code.push(` # Inner node: Error Handling (${innerId})`);
|
|
222
|
+
code.push(` try:`);
|
|
223
|
+
code.push(` # Error-handled operation`);
|
|
224
|
+
code.push(` pass`);
|
|
225
|
+
code.push(` except Exception as e:`);
|
|
226
|
+
code.push(` logger.error(f"Error in ${innerId}: {str(e)}")`);
|
|
227
|
+
code.push(` return {"error": str(e), "status": 500}`);
|
|
228
|
+
code.push('');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return code;
|
|
232
|
+
}
|
|
233
|
+
export default async function generatePythonFastAPIFiles(actualIR, opts = {}) {
|
|
234
|
+
const files = {};
|
|
235
|
+
const graph = actualIR && actualIR.graph ? actualIR.graph : actualIR || {};
|
|
236
|
+
const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
237
|
+
const edges = Array.isArray(graph.edges) ? graph.edges : [];
|
|
238
|
+
const handlers = [];
|
|
239
|
+
const routes = [];
|
|
240
|
+
const models = [];
|
|
241
|
+
const endpoints = [];
|
|
242
|
+
const nonHttpNodes = [];
|
|
243
|
+
// Track edge config utilities (retry, circuit breaker) to include once
|
|
244
|
+
const edgeUtilityCode = new Set();
|
|
245
|
+
for (const n of nodes) {
|
|
246
|
+
// expose cloudFunction and http nodes as FastAPI endpoints
|
|
247
|
+
if (!n || !n.type)
|
|
248
|
+
continue;
|
|
249
|
+
if (n.type === 'cloudFunction' || n.type === 'http' || n.type === 'httpRequest') {
|
|
250
|
+
const id = safeId(n.id || n.name);
|
|
251
|
+
const nodeConfig = n?.config || {};
|
|
252
|
+
const dataConfig = n?.data?.config || {};
|
|
253
|
+
const cfg = { ...nodeConfig, ...dataConfig };
|
|
254
|
+
// Extract businessLogic for implementation guidance
|
|
255
|
+
const businessLogic = cfg.businessLogic || n?.businessLogic || n?.description || null;
|
|
256
|
+
const method = String(cfg.method || 'post').toLowerCase();
|
|
257
|
+
const route = cfg.route || cfg.url ? String(cfg.route || cfg.url) : `/${id}`;
|
|
258
|
+
const isAsync = cfg.async === true || cfg.asyncProcessing === true || cfg.accepted === true;
|
|
259
|
+
const successCode = method === 'post' ? (isAsync ? 202 : 201) : method === 'delete' ? 204 : 200;
|
|
260
|
+
const hname = handlerNameFor(n);
|
|
261
|
+
const reqModelName = `Payload${id.replace(/[^A-Za-z0-9]/g, '') || 'Request'}`;
|
|
262
|
+
const respModelName = `Response${id.replace(/[^A-Za-z0-9]/g, '') || 'Response'}`;
|
|
263
|
+
const requestSchema = cfg.schema || cfg.fields;
|
|
264
|
+
const responseSchema = cfg.responseSchema || cfg.response_schema || cfg.response;
|
|
265
|
+
models.push(buildPydanticModel(reqModelName, requestSchema));
|
|
266
|
+
models.push(buildPydanticModel(respModelName, responseSchema));
|
|
267
|
+
endpoints.push({ route, method, hasBody: method !== 'get', success: successCode, responseSchema, requestSchema });
|
|
268
|
+
// Get inner nodes if they exist (support nodes embedded in this key node)
|
|
269
|
+
const innerNodes = n.innerNodes || [];
|
|
270
|
+
// Get edge configurations for incoming edges to this node
|
|
271
|
+
const nodeId = String(n.id || '');
|
|
272
|
+
const incomingEdges = edges.filter((e) => {
|
|
273
|
+
const targetId = String(e.to || e.target || '');
|
|
274
|
+
return targetId === nodeId;
|
|
275
|
+
});
|
|
276
|
+
// Collect edge configs and apply them
|
|
277
|
+
let edgeRetryConfig = null;
|
|
278
|
+
let edgeCircuitBreakerConfig = null;
|
|
279
|
+
let edgeTimeout = null;
|
|
280
|
+
for (const edge of incomingEdges) {
|
|
281
|
+
const sourceId = String(edge.from || edge.source || '');
|
|
282
|
+
const edgeConfig = getEdgeConfig(edges, sourceId, nodeId);
|
|
283
|
+
if (edgeConfig) {
|
|
284
|
+
// Apply edge timeout (override node timeout if edge has one)
|
|
285
|
+
if (edgeConfig.config?.timeout) {
|
|
286
|
+
edgeTimeout = edgeConfig.config.timeout;
|
|
287
|
+
}
|
|
288
|
+
// Collect retry config from edge (prefer edge over node)
|
|
289
|
+
if (edgeConfig.config?.retry?.maxAttempts && edgeConfig.config.retry.maxAttempts > 0) {
|
|
290
|
+
edgeRetryConfig = edgeConfig;
|
|
291
|
+
const retryCode = generateRetryCode(edgeConfig, 'python');
|
|
292
|
+
if (retryCode) {
|
|
293
|
+
edgeUtilityCode.add(retryCode);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Collect circuit breaker config from edge
|
|
297
|
+
if (edgeConfig.config?.circuitBreaker?.enabled) {
|
|
298
|
+
edgeCircuitBreakerConfig = edgeConfig;
|
|
299
|
+
const cbCode = generateCircuitBreakerCode(edgeConfig, 'python');
|
|
300
|
+
if (cbCode) {
|
|
301
|
+
edgeUtilityCode.add(cbCode);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Build handler code with inner nodes
|
|
307
|
+
const handlerLines = [];
|
|
308
|
+
handlerLines.push(`def ${hname}(payload: ${reqModelName} | None = None):`);
|
|
309
|
+
// Include businessLogic in docstring if available
|
|
310
|
+
const docstring = businessLogic
|
|
311
|
+
? ` """Handler for node ${String(n.id || '')}${innerNodes.length > 0 ? ` (with ${innerNodes.length} inner node(s))` : ''}\n \n Business Logic: ${businessLogic}\n """`
|
|
312
|
+
: ` """Handler for node ${String(n.id || '')}${innerNodes.length > 0 ? ` (with ${innerNodes.length} inner node(s))` : ''}"""`;
|
|
313
|
+
handlerLines.push(docstring);
|
|
314
|
+
if (businessLogic) {
|
|
315
|
+
handlerLines.push(` # Business Logic: ${businessLogic}`);
|
|
316
|
+
}
|
|
317
|
+
handlerLines.push(' # Rate limit / quota stub');
|
|
318
|
+
handlerLines.push(' # TODO: implement per-endpoint rate limiting');
|
|
319
|
+
handlerLines.push(' # Policy enforcement (fail open on error)');
|
|
320
|
+
handlerLines.push(' import asyncio');
|
|
321
|
+
handlerLines.push(' try:');
|
|
322
|
+
handlerLines.push(' ok = asyncio.get_event_loop().run_until_complete(enforce_policy({"nodeId": "' + String(n.id || '') + '", "route": "' + route + '", "method": "' + method + '"}))');
|
|
323
|
+
handlerLines.push(' if not ok:');
|
|
324
|
+
handlerLines.push(' return Response(status_code=403, content="policy_blocked")');
|
|
325
|
+
handlerLines.push(' except Exception:');
|
|
326
|
+
handlerLines.push(' logging.warning("Policy check failed; continuing");');
|
|
327
|
+
// Add inner node code first (authentication, validation, etc.)
|
|
328
|
+
if (innerNodes.length > 0) {
|
|
329
|
+
const innerCode = generateInnerNodeCode(innerNodes);
|
|
330
|
+
handlerLines.push(...innerCode);
|
|
331
|
+
}
|
|
332
|
+
// Add main business logic driven by config
|
|
333
|
+
handlerLines.push(' import asyncio');
|
|
334
|
+
handlerLines.push(' import time');
|
|
335
|
+
handlerLines.push(` config = ${JSON.stringify(cfg)}`);
|
|
336
|
+
handlerLines.push(' request_id = str(getattr(payload, "requestId", None) or "")');
|
|
337
|
+
handlerLines.push(' filters = getattr(payload, "filters", {}) if payload else {}');
|
|
338
|
+
handlerLines.push(' page = int(filters.get("page", filters.get("offset", 1)) or 1)');
|
|
339
|
+
handlerLines.push(' page_size = int(filters.get("pageSize", filters.get("limit", 20)) or 20)');
|
|
340
|
+
handlerLines.push(' status = filters.get("status")');
|
|
341
|
+
handlerLines.push(' date_from = filters.get("from") or filters.get("startDate")');
|
|
342
|
+
handlerLines.push(' date_to = filters.get("to") or filters.get("endDate")');
|
|
343
|
+
// Use edge timeout if available, otherwise fall back to node config
|
|
344
|
+
const effectiveTimeout = edgeTimeout ?? cfg.timeoutMs ?? 2000;
|
|
345
|
+
handlerLines.push(` timeout_ms = int(${effectiveTimeout})`);
|
|
346
|
+
// Use edge retry config if available, otherwise fall back to node config
|
|
347
|
+
const effectiveRetryPolicy = edgeRetryConfig?.config?.retry || cfg.retryPolicy || { "maxAttempts": 2, "backoffMs": 500 };
|
|
348
|
+
handlerLines.push(` retry_policy = ${JSON.stringify(effectiveRetryPolicy)}`);
|
|
349
|
+
handlerLines.push(' max_attempts = int(retry_policy.get("maxAttempts", 1))');
|
|
350
|
+
handlerLines.push(' backoff_ms = int(retry_policy.get("backoffMs", 200))');
|
|
351
|
+
const retryStrategy = edgeRetryConfig?.config?.retry?.strategy || 'exponential';
|
|
352
|
+
handlerLines.push(` operation = ${JSON.stringify(cfg.operation || 'read')}`);
|
|
353
|
+
handlerLines.push(` primary_key = ${JSON.stringify(cfg.primaryKey || 'id')}`);
|
|
354
|
+
handlerLines.push(` table = ${JSON.stringify(cfg.table || 'records')}`);
|
|
355
|
+
handlerLines.push(` base_query = ${JSON.stringify(cfg.query || '')}`);
|
|
356
|
+
handlerLines.push('');
|
|
357
|
+
handlerLines.push(' async def run_operation():');
|
|
358
|
+
// Use businessLogic to generate more specific implementation
|
|
359
|
+
if (businessLogic) {
|
|
360
|
+
handlerLines.push(` # Business Logic: ${businessLogic}`);
|
|
361
|
+
// Generate implementation based on businessLogic keywords
|
|
362
|
+
if (businessLogic.toLowerCase().includes('validate') || businessLogic.toLowerCase().includes('check')) {
|
|
363
|
+
handlerLines.push(' # Validation logic based on business requirements');
|
|
364
|
+
handlerLines.push(' if payload:');
|
|
365
|
+
handlerLines.push(' payload_dict = payload.dict() if hasattr(payload, "dict") else payload');
|
|
366
|
+
handlerLines.push(' # Add validation checks based on business rules');
|
|
367
|
+
}
|
|
368
|
+
if (businessLogic.toLowerCase().includes('fetch') || businessLogic.toLowerCase().includes('get') || businessLogic.toLowerCase().includes('retrieve')) {
|
|
369
|
+
handlerLines.push(' # Fetch/retrieve operation based on business logic');
|
|
370
|
+
}
|
|
371
|
+
if (businessLogic.toLowerCase().includes('create') || businessLogic.toLowerCase().includes('insert')) {
|
|
372
|
+
handlerLines.push(' # Create operation with business validation');
|
|
373
|
+
}
|
|
374
|
+
if (businessLogic.toLowerCase().includes('calculate') || businessLogic.toLowerCase().includes('compute')) {
|
|
375
|
+
handlerLines.push(' # Calculation logic based on business rules');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
handlerLines.push(' # Database operation honoring config/query/engine');
|
|
379
|
+
handlerLines.push(' if operation == "create":');
|
|
380
|
+
handlerLines.push(' body = payload.dict() if payload else {};');
|
|
381
|
+
handlerLines.push(' new_id = body.get(primary_key) or f"new-{int(time.time()*1000)}"');
|
|
382
|
+
handlerLines.push(' created = {**body, primary_key: new_id, "createdAt": datetime.utcnow().isoformat() + "Z", "table": table, "query": base_query}');
|
|
383
|
+
handlerLines.push(' return {"created": created}');
|
|
384
|
+
handlerLines.push(' if operation == "update":');
|
|
385
|
+
handlerLines.push(' body = payload.dict() if payload else {};');
|
|
386
|
+
handlerLines.push(' the_id = body.get(primary_key) or filters.get(primary_key)');
|
|
387
|
+
handlerLines.push(' if not the_id:');
|
|
388
|
+
handlerLines.push(' raise HTTPException(status_code=400, detail="missing primary key")');
|
|
389
|
+
handlerLines.push(' updated = {**body, primary_key: the_id, "updatedAt": datetime.utcnow().isoformat() + "Z", "table": table, "query": base_query}');
|
|
390
|
+
handlerLines.push(' return {"updated": updated}');
|
|
391
|
+
handlerLines.push(' if operation == "delete":');
|
|
392
|
+
handlerLines.push(' the_id = filters.get(primary_key) or ((payload.dict() if payload else {}).get(primary_key) if payload else None)');
|
|
393
|
+
handlerLines.push(' if not the_id:');
|
|
394
|
+
handlerLines.push(' raise HTTPException(status_code=400, detail="missing primary key")');
|
|
395
|
+
handlerLines.push(' return {"deleted": True, "id": the_id, "table": table, "query": base_query}');
|
|
396
|
+
handlerLines.push(' rows = [');
|
|
397
|
+
handlerLines.push(' {');
|
|
398
|
+
handlerLines.push(' primary_key: f"ORD-{page}-{i+1}",');
|
|
399
|
+
handlerLines.push(' "status": status or "pending",');
|
|
400
|
+
handlerLines.push(' "total": 100 + i,');
|
|
401
|
+
handlerLines.push(' "createdAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),');
|
|
402
|
+
handlerLines.push(' "table": table,');
|
|
403
|
+
handlerLines.push(' "query": base_query or None');
|
|
404
|
+
handlerLines.push(' } for i in range(min(page_size, 5))');
|
|
405
|
+
handlerLines.push(' ]');
|
|
406
|
+
handlerLines.push(' return rows');
|
|
407
|
+
handlerLines.push('');
|
|
408
|
+
handlerLines.push(' data = None');
|
|
409
|
+
// Use circuit breaker if configured on edge
|
|
410
|
+
if (edgeCircuitBreakerConfig) {
|
|
411
|
+
handlerLines.push(' # Circuit breaker protection (from edge config)');
|
|
412
|
+
handlerLines.push(` circuit_breaker = CircuitBreaker(${edgeCircuitBreakerConfig.config?.circuitBreaker?.failureThreshold || 5}, ${(edgeCircuitBreakerConfig.config?.circuitBreaker?.resetTimeoutMs || 60000) / 1000})`);
|
|
413
|
+
handlerLines.push(' try:');
|
|
414
|
+
handlerLines.push(' # Wrap async operation for circuit breaker');
|
|
415
|
+
handlerLines.push(' async def protected_operation():');
|
|
416
|
+
handlerLines.push(' return await asyncio.wait_for(run_operation(), timeout_ms / 1000)');
|
|
417
|
+
handlerLines.push(' # Circuit breaker expects sync function, so we run async in executor');
|
|
418
|
+
handlerLines.push(' loop = asyncio.get_event_loop()');
|
|
419
|
+
handlerLines.push(' data = await loop.run_in_executor(None, lambda: circuit_breaker.call(lambda: loop.run_until_complete(protected_operation())))');
|
|
420
|
+
handlerLines.push(' except Exception as err:');
|
|
421
|
+
handlerLines.push(' logger.error("circuit breaker open or operation failed", exc_info=True)');
|
|
422
|
+
handlerLines.push(' return Response(status_code=500, content="upstream_failed")');
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
// Use retry logic (from edge or node config)
|
|
426
|
+
const useEdgeRetry = edgeRetryConfig && retryStrategy === 'exponential';
|
|
427
|
+
if (useEdgeRetry) {
|
|
428
|
+
handlerLines.push(' # Retry with exponential backoff (from edge config)');
|
|
429
|
+
handlerLines.push(' # Note: Edge retry utility is sync, so we adapt for async');
|
|
430
|
+
handlerLines.push(' for attempt in range(max_attempts):');
|
|
431
|
+
handlerLines.push(' try:');
|
|
432
|
+
handlerLines.push(' data = await asyncio.wait_for(run_operation(), timeout_ms / 1000)');
|
|
433
|
+
handlerLines.push(' break');
|
|
434
|
+
handlerLines.push(' except Exception as err:');
|
|
435
|
+
handlerLines.push(' if attempt == max_attempts - 1:');
|
|
436
|
+
handlerLines.push(' logger.error("upstream_failed", exc_info=True)');
|
|
437
|
+
handlerLines.push(' return Response(status_code=500, content="upstream_failed")');
|
|
438
|
+
handlerLines.push(' # Exponential backoff: base_delay * (2 ** attempt)');
|
|
439
|
+
handlerLines.push(' delay = (backoff_ms / 1000) * (2 ** attempt)');
|
|
440
|
+
handlerLines.push(' await asyncio.sleep(delay)');
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
// Fallback to standard retry loop
|
|
444
|
+
handlerLines.push(' for attempt in range(1, max_attempts + 1):');
|
|
445
|
+
handlerLines.push(' try:');
|
|
446
|
+
handlerLines.push(' # In real code, apply timeout using asyncio.wait_for');
|
|
447
|
+
handlerLines.push(' data = await asyncio.wait_for(run_operation(), timeout_ms / 1000);');
|
|
448
|
+
handlerLines.push(' break');
|
|
449
|
+
handlerLines.push(' except Exception as err:');
|
|
450
|
+
handlerLines.push(' if attempt == max_attempts:');
|
|
451
|
+
handlerLines.push(' logger.error("upstream_failed", exc_info=True)');
|
|
452
|
+
handlerLines.push(' return Response(status_code=500, content="upstream_failed")');
|
|
453
|
+
handlerLines.push(' await asyncio.sleep(backoff_ms / 1000)');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
handlerLines.push('');
|
|
457
|
+
handlerLines.push(' logger.info({');
|
|
458
|
+
handlerLines.push(' "event": "audit",');
|
|
459
|
+
handlerLines.push(' "route": "' + route + '",');
|
|
460
|
+
handlerLines.push(' "status": "success",');
|
|
461
|
+
handlerLines.push(' "filters": {');
|
|
462
|
+
handlerLines.push(' "status": status, "date_from": date_from, "date_to": date_to, "page": page, "page_size": page_size');
|
|
463
|
+
handlerLines.push(' }');
|
|
464
|
+
handlerLines.push(' })');
|
|
465
|
+
if (successCode === 204) {
|
|
466
|
+
handlerLines.push(' return Response(status_code=204)');
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
handlerLines.push(` return ${respModelName}(status="ok", message="Handled ${String(n.name || n.id || '')}", data={"items": data, "page": page, "pageSize": page_size})`);
|
|
470
|
+
}
|
|
471
|
+
handlerLines.push('');
|
|
472
|
+
handlers.push(handlerLines.join('\n'));
|
|
473
|
+
// route wrapper
|
|
474
|
+
const routeLines = [];
|
|
475
|
+
const statusCode = successCode;
|
|
476
|
+
const responseModelPart = statusCode === 204 ? 'response_model=None' : `response_model=${respModelName}`;
|
|
477
|
+
const methodDecorator = method === 'get'
|
|
478
|
+
? `@app.get("${route}", ${responseModelPart}, status_code=${statusCode})`
|
|
479
|
+
: `@app.post("${route}", ${responseModelPart}, status_code=${statusCode})`;
|
|
480
|
+
routeLines.push(methodDecorator);
|
|
481
|
+
routeLines.push(`async def ${hname}_endpoint(request):`);
|
|
482
|
+
routeLines.push(' try:');
|
|
483
|
+
routeLines.push(' payload_dict = await request.json()');
|
|
484
|
+
routeLines.push(' except Exception:');
|
|
485
|
+
routeLines.push(' payload_dict = {}');
|
|
486
|
+
routeLines.push(` payload = ${reqModelName}(**payload_dict) if payload_dict else None`);
|
|
487
|
+
routeLines.push(` return ${hname}(payload)`);
|
|
488
|
+
routeLines.push('');
|
|
489
|
+
routes.push(routeLines.join('\n'));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Collect non-http nodes
|
|
493
|
+
nodes.forEach((n) => {
|
|
494
|
+
if (!n || !n.type)
|
|
495
|
+
return;
|
|
496
|
+
const t = String(n.type || '').toLowerCase();
|
|
497
|
+
if (t.includes('http'))
|
|
498
|
+
return;
|
|
499
|
+
const cfg = n.config || n.data?.config || {};
|
|
500
|
+
nonHttpNodes.push({
|
|
501
|
+
id: String(n.id || ''),
|
|
502
|
+
type: String(n.type || ''),
|
|
503
|
+
name: String(n.name || n.id || ''),
|
|
504
|
+
schema: cfg.schema || cfg.fields,
|
|
505
|
+
config: cfg,
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
// Helper functions for inner nodes
|
|
509
|
+
const helperFunctions = [];
|
|
510
|
+
helperFunctions.push('# Helper functions for inner nodes (support nodes)');
|
|
511
|
+
helperFunctions.push('import asyncio');
|
|
512
|
+
helperFunctions.push('import logging');
|
|
513
|
+
helperFunctions.push('');
|
|
514
|
+
helperFunctions.push('logger = logging.getLogger(__name__)');
|
|
515
|
+
helperFunctions.push('');
|
|
516
|
+
helperFunctions.push('def authenticate_request(request):');
|
|
517
|
+
helperFunctions.push(' """Authentication helper for inner nodes"""');
|
|
518
|
+
helperFunctions.push(' # Implement authentication based on request headers');
|
|
519
|
+
helperFunctions.push(' auth_header = request.headers.get("Authorization", "") if hasattr(request, "headers") else ""');
|
|
520
|
+
helperFunctions.push(' if auth_header.startswith("Bearer "):');
|
|
521
|
+
helperFunctions.push(' token = auth_header[7:]');
|
|
522
|
+
helperFunctions.push(' # Validate JWT token (implement actual validation)');
|
|
523
|
+
helperFunctions.push(' # For now, return valid if token exists');
|
|
524
|
+
helperFunctions.push(' return {"valid": bool(token), "user": {"id": "user-123", "token": token[:10] + "..."} if token else None}');
|
|
525
|
+
helperFunctions.push(' return {"valid": False, "user": None, "error": "Missing or invalid authorization"}');
|
|
526
|
+
helperFunctions.push('');
|
|
527
|
+
helperFunctions.push('def validate_schema(payload: dict):');
|
|
528
|
+
helperFunctions.push(' """Schema validation helper for inner nodes"""');
|
|
529
|
+
helperFunctions.push(' # Implement schema validation based on payload structure');
|
|
530
|
+
helperFunctions.push(' errors = []');
|
|
531
|
+
helperFunctions.push(' if not payload:');
|
|
532
|
+
helperFunctions.push(' return {"valid": False, "errors": ["Payload is required"]}');
|
|
533
|
+
helperFunctions.push(' # Add field-specific validation based on schema');
|
|
534
|
+
helperFunctions.push(' # Example: check required fields, types, ranges, etc.');
|
|
535
|
+
helperFunctions.push(' return {"valid": len(errors) == 0, "errors": errors}');
|
|
536
|
+
helperFunctions.push('');
|
|
537
|
+
helperFunctions.push('def parse_payload(payload: dict):');
|
|
538
|
+
helperFunctions.push(' """Payload parsing helper for inner nodes"""');
|
|
539
|
+
helperFunctions.push(' # Parse and normalize payload data');
|
|
540
|
+
helperFunctions.push(' if isinstance(payload, str):');
|
|
541
|
+
helperFunctions.push(' import json');
|
|
542
|
+
helperFunctions.push(' try:');
|
|
543
|
+
helperFunctions.push(' return json.loads(payload)');
|
|
544
|
+
helperFunctions.push(' except json.JSONDecodeError:');
|
|
545
|
+
helperFunctions.push(' return {"error": "Invalid JSON", "raw": payload}');
|
|
546
|
+
helperFunctions.push(' # Normalize dict keys, handle nested structures');
|
|
547
|
+
helperFunctions.push(' if isinstance(payload, dict):');
|
|
548
|
+
helperFunctions.push(' return {k: v for k, v in payload.items() if v is not None}');
|
|
549
|
+
helperFunctions.push(' return payload');
|
|
550
|
+
helperFunctions.push('');
|
|
551
|
+
// Add edge config utilities (retry, circuit breaker) if any were generated
|
|
552
|
+
if (edgeUtilityCode.size > 0) {
|
|
553
|
+
helperFunctions.push('# Edge configuration utilities (retry, circuit breaker)');
|
|
554
|
+
edgeUtilityCode.forEach(code => helperFunctions.push(code));
|
|
555
|
+
}
|
|
556
|
+
// main app file
|
|
557
|
+
const appLines = [];
|
|
558
|
+
appLines.push('from fastapi import FastAPI, Request, Response');
|
|
559
|
+
appLines.push('from typing import Dict, Any, List');
|
|
560
|
+
appLines.push('from pydantic import BaseModel');
|
|
561
|
+
appLines.push('import time');
|
|
562
|
+
appLines.push('import uuid');
|
|
563
|
+
appLines.push('import logging');
|
|
564
|
+
appLines.push('import httpx');
|
|
565
|
+
appLines.push('from fastapi.middleware.cors import CORSMiddleware');
|
|
566
|
+
appLines.push('');
|
|
567
|
+
appLines.push("app = FastAPI(title=\"Generated FastAPI App\")");
|
|
568
|
+
appLines.push('');
|
|
569
|
+
appLines.push("# CORS tightened via ALLOWED_ORIGINS env; defaults to none");
|
|
570
|
+
appLines.push("import os");
|
|
571
|
+
appLines.push("allowed = [o for o in os.getenv('ALLOWED_ORIGINS', '').split(',') if o]");
|
|
572
|
+
appLines.push("if allowed:");
|
|
573
|
+
appLines.push(" app.add_middleware(CORSMiddleware, allow_origins=allowed, allow_credentials=True, allow_methods=['*'], allow_headers=['*'])");
|
|
574
|
+
appLines.push('');
|
|
575
|
+
appLines.push('# Middlewares');
|
|
576
|
+
appLines.push('@app.middleware("http")');
|
|
577
|
+
appLines.push('async def runtime_middleware(request: Request, call_next):');
|
|
578
|
+
appLines.push(' rid = request.headers.get("x-request-id", str(uuid.uuid4()))');
|
|
579
|
+
appLines.push(' start = time.time()');
|
|
580
|
+
appLines.push(' request.state.request_id = rid');
|
|
581
|
+
appLines.push(' try:');
|
|
582
|
+
appLines.push(' response = await call_next(request)');
|
|
583
|
+
appLines.push(' except Exception as exc:');
|
|
584
|
+
appLines.push(' logging.exception("Request failed");');
|
|
585
|
+
appLines.push(' return Response(status_code=500, content="internal error")');
|
|
586
|
+
appLines.push(' duration = int((time.time() - start) * 1000)');
|
|
587
|
+
appLines.push(' response.headers["x-request-id"] = rid');
|
|
588
|
+
appLines.push(' response.headers["x-duration-ms"] = str(duration)');
|
|
589
|
+
appLines.push(' return response');
|
|
590
|
+
appLines.push('');
|
|
591
|
+
appLines.push('async def enforce_policy(payload: dict) -> bool:');
|
|
592
|
+
appLines.push(' url = ""');
|
|
593
|
+
appLines.push(' api_key = ""');
|
|
594
|
+
appLines.push(' if not url:');
|
|
595
|
+
appLines.push(' return True');
|
|
596
|
+
appLines.push(' try:');
|
|
597
|
+
appLines.push(' async with httpx.AsyncClient() as client:');
|
|
598
|
+
appLines.push(' headers = {"x-api-key": api_key} if api_key else {}');
|
|
599
|
+
appLines.push(' resp = await client.post(url, json=payload, headers=headers)');
|
|
600
|
+
appLines.push(' return resp.status_code < 300');
|
|
601
|
+
appLines.push(' except Exception:');
|
|
602
|
+
appLines.push(' logging.warning("Policy enforcement failed", exc_info=True)');
|
|
603
|
+
appLines.push(' return True # fail open');
|
|
604
|
+
appLines.push('');
|
|
605
|
+
// models
|
|
606
|
+
appLines.push('# Models derived from node schemas');
|
|
607
|
+
appLines.push(models.join('\n'));
|
|
608
|
+
// include helper functions
|
|
609
|
+
appLines.push(helperFunctions.join('\n'));
|
|
610
|
+
// include handlers
|
|
611
|
+
appLines.push('# Handlers');
|
|
612
|
+
appLines.push(handlers.join('\n'));
|
|
613
|
+
appLines.push('');
|
|
614
|
+
appLines.push('# Routes');
|
|
615
|
+
appLines.push(routes.join('\n'));
|
|
616
|
+
appLines.push('');
|
|
617
|
+
appLines.push('@app.get("/healthz")');
|
|
618
|
+
appLines.push('async def healthz():');
|
|
619
|
+
appLines.push(' return {"ok": True}');
|
|
620
|
+
appLines.push('');
|
|
621
|
+
appLines.push('@app.get("/ready")');
|
|
622
|
+
appLines.push('async def ready():');
|
|
623
|
+
appLines.push(' return {"ok": True}');
|
|
624
|
+
appLines.push('');
|
|
625
|
+
appLines.push("if __name__ == '__main__':");
|
|
626
|
+
appLines.push(" import uvicorn");
|
|
627
|
+
appLines.push(" uvicorn.run(app, host='0.0.0.0', port=8080)");
|
|
628
|
+
files['app/main.py'] = appLines.join('\n');
|
|
629
|
+
// requirements
|
|
630
|
+
files['requirements.txt'] = 'fastapi\nuvicorn\nhttpx\npydantic\n';
|
|
631
|
+
// pyproject.toml — single project root (like .sln for C#, package.json for Node)
|
|
632
|
+
const projectName = String((opts && opts.projectName) || (actualIR && actualIR.metadata && actualIR.metadata.name) || 'generated-fastapi').replace(/[^A-Za-z0-9_-]/g, '-');
|
|
633
|
+
files['pyproject.toml'] = `[build-system]
|
|
634
|
+
requires = ["setuptools>=61.0"]
|
|
635
|
+
build-backend = "setuptools.build_meta"
|
|
636
|
+
|
|
637
|
+
[project]
|
|
638
|
+
name = "${projectName}"
|
|
639
|
+
version = "0.1.0"
|
|
640
|
+
description = "Generated FastAPI project"
|
|
641
|
+
requires-python = ">=3.10"
|
|
642
|
+
dependencies = [
|
|
643
|
+
"fastapi>=0.104.0",
|
|
644
|
+
"uvicorn>=0.24.0",
|
|
645
|
+
"httpx>=0.25.0",
|
|
646
|
+
"pydantic>=2.0.0",
|
|
647
|
+
]
|
|
648
|
+
`;
|
|
649
|
+
files['tests/contract.test.js'] = [
|
|
650
|
+
"const fs = require('fs');",
|
|
651
|
+
"const path = require('path');",
|
|
652
|
+
"describe('contract', () => {",
|
|
653
|
+
" it('should have an openapi file', () => {",
|
|
654
|
+
" const spec = fs.readFileSync(path.join(__dirname, '../openapi.yaml'), 'utf-8');",
|
|
655
|
+
" expect(spec).toBeTruthy();",
|
|
656
|
+
" });",
|
|
657
|
+
"});",
|
|
658
|
+
].join('\n');
|
|
659
|
+
files['openapi.yaml'] = buildOpenApiSpec(opts.projectName || (actualIR && actualIR.metadata && actualIR.metadata.name) || 'generated-fastapi', endpoints, nonHttpNodes);
|
|
660
|
+
// README
|
|
661
|
+
const projectDisplayName = String((opts && opts.projectName) || (actualIR && actualIR.metadata && actualIR.metadata.name) || 'generated-fastapi');
|
|
662
|
+
files['README.md'] = `# ${projectDisplayName}\n\nGenerated FastAPI project.\n\nRun:\n\n pip install -r requirements.txt\n python app/main.py\n`;
|
|
663
|
+
return files;
|
|
664
|
+
}
|