Skip to content

Commit e123a0a

Browse files
authored
Merge pull request #3524 from hey-api/chore/py-sdk-params
chore: python sdk params
2 parents cf5ae58 + 638429d commit e123a0a

38 files changed

Lines changed: 662 additions & 141 deletions

File tree

dev/playground.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
def run():
55
client = OpenCode()
6-
client.tui.publish()
7-
# body={
8-
# "properties": {
9-
# "message": "Hello from Hey API OpenAPI TS Playground!",
10-
# "variant": "success",
11-
# },
12-
# "type": "tui.toast.show",
13-
# },
14-
# directory="main",
6+
client.tui.publish(
7+
body={
8+
"properties": {
9+
"message": "Hello from Hey API OpenAPI Python Playground!",
10+
"variant": "success",
11+
},
12+
"type": "tui.toast.show",
13+
},
14+
directory="main",
15+
)
1516

1617
run()

dev/playground.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async function run() {
2727
sdk.tui.publish({
2828
body: {
2929
properties: {
30-
message: 'Hello from Hey API OpenAPI TS Playground!',
30+
message: 'Hello from Hey API OpenAPI TypeScript Playground!',
3131
variant: 'success',
3232
},
3333
type: 'tui.toast.show',
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import type { IR } from '@hey-api/shared';
2+
import { toCase } from '@hey-api/shared';
3+
4+
import type { $ } from '../../../../py-dsl';
5+
// import { py } from '../../../../ts-python';
6+
import type { HeyApiSdkPlugin } from '../types';
7+
import { getSignatureParameters } from './signature';
8+
9+
type OperationParameters = {
10+
bodyRef?: string;
11+
parameters: Array<ReturnType<typeof $.param>>;
12+
// parameters: Array<{
13+
// annotation?: py.Expression;
14+
// defaultValue?: py.Expression;
15+
// name: string;
16+
// }>;
17+
};
18+
19+
const PYTHON_BUILTIN_TYPES: Record<string, string> = {
20+
array: 'list',
21+
boolean: 'bool',
22+
integer: 'int',
23+
number: 'float',
24+
object: 'dict',
25+
string: 'str',
26+
};
27+
28+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
29+
function schemaToPythonType(schema: IR.SchemaObject, plugin: HeyApiSdkPlugin['Instance']): string {
30+
if (schema.$ref) {
31+
return toCase(schema.$ref.split('/').pop()!, 'PascalCase');
32+
}
33+
34+
if (schema.type === 'array') {
35+
const itemsSchema = schema.items as IR.SchemaObject | undefined;
36+
const itemType = itemsSchema ? schemaToPythonType(itemsSchema, plugin) : 'Any';
37+
return `list[${itemType}]`;
38+
}
39+
40+
if (schema.type === 'object' || schema.additionalProperties) {
41+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
42+
const valueType = schemaToPythonType(schema.additionalProperties as IR.SchemaObject, plugin);
43+
return `dict[str, ${valueType}]`;
44+
}
45+
return 'dict[str, Any]';
46+
}
47+
48+
if (schema.type === 'tuple') {
49+
const itemsSchema = schema.items as IR.SchemaObject | IR.SchemaObject[] | undefined;
50+
const itemTypes = itemsSchema
51+
? Array.isArray(itemsSchema)
52+
? itemsSchema.map((item) => schemaToPythonType(item, plugin))
53+
: [schemaToPythonType(itemsSchema, plugin)]
54+
: [];
55+
return `tuple[${itemTypes.join(', ')}]`;
56+
}
57+
58+
const builtinType = schema.type ? PYTHON_BUILTIN_TYPES[schema.type] : 'Any';
59+
return builtinType ?? 'Any';
60+
}
61+
62+
export function operationParameters({
63+
operation,
64+
plugin,
65+
}: {
66+
operation: IR.OperationObject;
67+
plugin: HeyApiSdkPlugin['Instance'];
68+
}): OperationParameters {
69+
const result: OperationParameters = {
70+
parameters: [],
71+
};
72+
73+
if (plugin.config.paramsStructure === 'flat') {
74+
const signature = getSignatureParameters({ operation });
75+
if (!signature) return result;
76+
77+
// result.bodyRef = signature.bodyRef;
78+
79+
// for (const param of opParameters.parameters) {
80+
// if (param.name === '*') {
81+
// continue;
82+
// }
83+
// node.param(param.name, (p) => p.type(param.annotation).default(param.defaultValue));
84+
// }
85+
86+
// const pathParams: OperationParameters['parameters'] = [];
87+
// const requiredParams: OperationParameters['parameters'] = [];
88+
// const optionalParams: OperationParameters['parameters'] = [];
89+
90+
// const paramNames = Object.keys(signature.parameters);
91+
92+
// for (const paramName of paramNames) {
93+
// const param = signature.parameters[paramName]!;
94+
95+
// if (param.in === 'path') {
96+
// const type = schemaToPythonType(param.schema, plugin);
97+
// pathParams.push({
98+
// annotation: py.factory.createIdentifier(type),
99+
// name: param.name,
100+
// });
101+
// continue;
102+
// }
103+
104+
// if (param.in === 'body' && param.schema.$ref) {
105+
// const refName = toCase(param.schema.$ref.split('/').pop()!, 'PascalCase');
106+
// if (param.isRequired) {
107+
// requiredParams.push({
108+
// annotation: py.factory.createIdentifier(refName),
109+
// name: param.name,
110+
// });
111+
// } else {
112+
// optionalParams.push({
113+
// annotation: py.factory.createIdentifier(`${refName} | None`),
114+
// defaultValue: py.factory.createLiteral(null),
115+
// name: param.name,
116+
// });
117+
// }
118+
// continue;
119+
// }
120+
121+
// const type = schemaToPythonType(param.schema, plugin);
122+
123+
// if (param.isRequired) {
124+
// requiredParams.push({
125+
// annotation: py.factory.createIdentifier(type),
126+
// name: param.name,
127+
// });
128+
// } else {
129+
// let defaultValue: py.Expression = py.factory.createLiteral(null);
130+
// if (param.schema.default !== undefined) {
131+
// const defaultVal = param.schema.default;
132+
// if (
133+
// typeof defaultVal === 'string' ||
134+
// typeof defaultVal === 'number' ||
135+
// typeof defaultVal === 'boolean'
136+
// ) {
137+
// defaultValue = py.factory.createLiteral(defaultVal);
138+
// } else {
139+
// defaultValue = py.factory.createLiteral(null);
140+
// }
141+
// } else if (type.startsWith('list') || type.startsWith('dict')) {
142+
// defaultValue = py.factory.createLiteral(null);
143+
// }
144+
145+
// optionalParams.push({
146+
// annotation: py.factory.createIdentifier(`${type} | None`),
147+
// defaultValue,
148+
// name: param.name,
149+
// });
150+
// }
151+
// }
152+
153+
// if (pathParams.length > 0) {
154+
// result.parameters.push(...pathParams);
155+
// }
156+
157+
// if (requiredParams.length > 0 || optionalParams.length > 0) {
158+
// result.parameters.push({ name: '*' });
159+
// result.parameters.push(...requiredParams);
160+
// result.parameters.push(...optionalParams);
161+
// }
162+
163+
// result.parameters.push({
164+
// annotation: py.factory.createIdentifier('float | None'),
165+
// defaultValue: py.factory.createLiteral(null),
166+
// name: 'timeout',
167+
// });
168+
}
169+
170+
return result;
171+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import type { IR } from '@hey-api/shared';
2+
import { refToName, toCase } from '@hey-api/shared';
3+
4+
type Location = keyof IR.ParametersObject | 'body';
5+
6+
type SignatureParameter = {
7+
in: Location;
8+
isRequired: boolean;
9+
name: string;
10+
originalName?: string;
11+
schema: IR.SchemaObject;
12+
};
13+
14+
type SignatureParameters = Record<string, SignatureParameter>;
15+
16+
type Signature = {
17+
bodyRef?: string;
18+
parameters: SignatureParameters;
19+
};
20+
21+
export function getSignatureParameters({
22+
operation,
23+
}: {
24+
operation: IR.OperationObject;
25+
}): Signature | undefined {
26+
const locations = ['header', 'path', 'query'] as const satisfies ReadonlyArray<Location>;
27+
const nameToLocations: Record<string, Set<Location>> = {};
28+
29+
const addParameter = (name: string, location: Location): void => {
30+
if (!nameToLocations[name]) {
31+
nameToLocations[name] = new Set();
32+
}
33+
nameToLocations[name]!.add(location);
34+
};
35+
36+
for (const location of locations) {
37+
const parameters = operation.parameters?.[location];
38+
if (parameters) {
39+
for (const key in parameters) {
40+
const parameter = parameters[key]!;
41+
addParameter(parameter.name, location);
42+
}
43+
}
44+
}
45+
46+
if (operation.body) {
47+
if (
48+
!operation.body.schema.logicalOperator &&
49+
operation.body.schema.type === 'object' &&
50+
operation.body.schema.properties
51+
) {
52+
const properties = operation.body.schema.properties;
53+
for (const key in properties) {
54+
addParameter(key, 'body');
55+
}
56+
} else if (operation.body.schema.$ref) {
57+
const name = refToName(operation.body.schema.$ref);
58+
const key = toCase(name, 'snake_case');
59+
addParameter(key, 'body');
60+
} else {
61+
addParameter('body', 'body');
62+
}
63+
}
64+
65+
const conflicts = new Set<string>();
66+
for (const name in nameToLocations) {
67+
if (nameToLocations[name]!.size > 1) {
68+
conflicts.add(name);
69+
}
70+
}
71+
72+
const signatureParameters: SignatureParameters = {};
73+
74+
for (const location of locations) {
75+
const parameters = operation.parameters?.[location];
76+
if (parameters) {
77+
for (const key in parameters) {
78+
const parameter = parameters[key]!;
79+
const originalName = parameter.name;
80+
const name = conflicts.has(originalName) ? `${location}_${originalName}` : originalName;
81+
const signatureParameter: SignatureParameter = {
82+
in: location,
83+
isRequired: parameter.required ?? false,
84+
name,
85+
schema: parameter.schema,
86+
};
87+
if (name !== originalName) {
88+
signatureParameter.originalName = originalName;
89+
}
90+
signatureParameters[name] = signatureParameter;
91+
}
92+
}
93+
}
94+
95+
let bodyRef: string | undefined;
96+
97+
if (operation.body) {
98+
const location = 'body';
99+
if (
100+
!operation.body.schema.logicalOperator &&
101+
operation.body.schema.type === 'object' &&
102+
operation.body.schema.properties
103+
) {
104+
const properties = operation.body.schema.properties;
105+
for (const originalName in properties) {
106+
const property = properties[originalName]!;
107+
const name = conflicts.has(originalName) ? `${location}_${originalName}` : originalName;
108+
const signatureParameter: SignatureParameter = {
109+
in: location,
110+
isRequired: operation.body.schema.required?.includes(originalName) ?? false,
111+
name,
112+
schema: property,
113+
};
114+
if (name !== originalName) {
115+
signatureParameter.originalName = originalName;
116+
}
117+
signatureParameters[name] = signatureParameter;
118+
}
119+
} else if (operation.body.schema.$ref) {
120+
const value = refToName(operation.body.schema.$ref);
121+
const originalName = toCase(value, 'snake_case');
122+
const name = conflicts.has(originalName) ? `${location}_${originalName}` : originalName;
123+
bodyRef = toCase(value, 'PascalCase');
124+
const signatureParameter: SignatureParameter = {
125+
in: location,
126+
isRequired: operation.body.required ?? false,
127+
name,
128+
schema: operation.body.schema,
129+
};
130+
if (name !== originalName) {
131+
signatureParameter.originalName = originalName;
132+
}
133+
signatureParameters[name] = signatureParameter;
134+
} else {
135+
signatureParameters.body = {
136+
in: location,
137+
isRequired: operation.body.required ?? false,
138+
name: 'body',
139+
schema: operation.body.schema,
140+
};
141+
}
142+
}
143+
144+
if (!Object.keys(signatureParameters).length) {
145+
return;
146+
}
147+
148+
return { bodyRef, parameters: signatureParameters };
149+
}

packages/openapi-python/src/plugins/@hey-api/sdk/v1/node.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { applyNaming, toCase } from '@hey-api/shared';
1010

1111
import { $ } from '../../../../py-dsl';
1212
import { createOperationComment } from '../../../shared/utils/operation';
13+
import { operationParameters } from '../shared/operation';
1314
import type { HeyApiSdkPlugin } from '../types';
1415

1516
export interface OperationItem {
@@ -114,13 +115,15 @@ function implementFn<T extends ReturnType<typeof $.func>>(args: {
114115
operation: IR.OperationObject;
115116
plugin: HeyApiSdkPlugin['Instance'];
116117
}): T {
117-
const { node, operation } = args;
118-
119-
const method = operation.method?.toLowerCase() || 'get';
120-
121-
node.do($('self').attr('client').attr(method).call($.literal(operation.path)).return());
122-
123-
return node;
118+
const { node, operation, plugin } = args;
119+
const method = operation.method.toLowerCase();
120+
const opParameters = operationParameters({ operation, plugin });
121+
return (
122+
node
123+
.params(...opParameters.parameters)
124+
// TODO: extract operation statements into a separate function
125+
.do($('self').attr('client').attr(method).call($.literal(operation.path)).return()) as T
126+
);
124127
}
125128

126129
export function toNode(

0 commit comments

Comments
 (0)