import { isHttpMetadata, SdkArrayType, SdkBuiltInType, SdkConstantType, SdkCredentialType, SdkDateTimeType, SdkDictionaryType, SdkDurationType, SdkEndpointType, SdkEnumType, SdkEnumValueType, SdkModelPropertyType, SdkModelType, SdkType, SdkUnionType, UsageFlags, } from "@azure-tools/typespec-client-generator-core"; import { getEncode, Type } from "@typespec/compiler"; import { HttpAuth, Visibility } from "@typespec/http"; import { dump } from "js-yaml"; import { PythonSdkContext } from "./lib.js"; import { camelToSnakeCase, emitParamBase, getAddedOn, getClientName, getClientNamespace, getImplementation, } from "./utils.js"; export interface CredentialType { kind: "Credential"; scheme: HttpAuth; } export interface CredentialTypeUnion { kind: "CredentialTypeUnion"; types: CredentialType[]; } interface MultiPartFileType { kind: "multipartfile"; type: SdkType; } function isEmptyModel(type: SdkType): boolean { // object, {} will be treated as empty model, user defined empty model will not return ( type.kind === "model" && type.properties.length === 0 && !type.baseModel && !type.discriminatedSubtypes && !type.discriminatorValue && (type.isGeneratedName || type.name === "object") ); } export function getSimpleTypeResult( context: PythonSdkContext, result: Record, ): Record { const key = dump(result, { sortKeys: true }); const value = context.__simpleTypesMap.get(key); if (value) { result = value; } else { context.__simpleTypesMap.set(key, result); } return result; } export function getType( context: PythonSdkContext, type: CredentialType | CredentialTypeUnion | Type | SdkType | MultiPartFileType, ): Record { switch (type.kind) { case "model": return emitModel(context, type); case "union": return emitUnion(context, type); case "enum": return emitEnum(context, type); case "constant": return emitConstant(context, type)!; case "array": case "dict": return emitArrayOrDict(context, type)!; case "utcDateTime": case "offsetDateTime": case "duration": return emitDurationOrDateType(context, type); case "enumvalue": return emitEnumMember(context, type, emitEnum(context, type.enumType)); case "credential": return emitCredential(context, type); case "bytes": case "boolean": case "plainDate": case "plainTime": case "numeric": case "integer": case "safeint": case "int8": case "uint8": case "int16": case "uint16": case "int32": case "uint32": case "int64": case "uint64": case "float": case "float32": case "float64": case "decimal": case "decimal128": case "string": case "url": return emitBuiltInType(context, type); case "unknown": return KnownTypes.any; case "nullable": return getType(context, type.type); case "multipartfile": return emitMultiPartFile(context, type); default: throw Error(`Not supported ${type.kind}`); } } function emitMultiPartFile( context: PythonSdkContext, type: MultiPartFileType, ): Record { if (type.type.kind === "array") { return getSimpleTypeResult(context, { type: "list", elementType: getType(context, createMultiPartFileType(type.type.valueType)), }); } return getSimpleTypeResult(context, { type: type.kind, description: type.type.summary ? type.type.summary : type.type.doc, }); } function emitCredential( context: PythonSdkContext, credential: SdkCredentialType, ): Record { let credential_type: Record = {}; const scheme = credential.scheme; if (scheme.type === "oauth2") { credential_type = { type: "OAuth2", policy: { type: "BearerTokenCredentialPolicy", credentialScopes: [], flows: (context.emitContext.options as any).flavor === "azure" ? [] : scheme.flows, }, }; for (const flow of scheme.flows) { for (const scope of flow.scopes) { credential_type.policy.credentialScopes.push(scope.value); } credential_type.policy.credentialScopes.push(); } } else if (scheme.type === "apiKey") { credential_type = { type: "Key", policy: { type: "KeyCredentialPolicy", key: scheme.name, }, }; } else if (scheme.type === "http") { credential_type = { type: "Key", policy: { type: "KeyCredentialPolicy", key: "Authorization", scheme: scheme.scheme[0].toUpperCase() + scheme.scheme.slice(1), }, }; } return getSimpleTypeResult(context, credential_type); } function visibilityMapping(visibility?: Visibility[]): string[] | undefined { if (visibility === undefined) { return undefined; } const result = []; for (const v of visibility) { if (v === Visibility.Read) { result.push("read"); } else if (v === Visibility.Create) { result.push("create"); } else if (v === Visibility.Update) { result.push("update"); } else if (v === Visibility.Delete) { result.push("delete"); } else if (v === Visibility.Query) { result.push("query"); } } return result; } function createMultiPartFileType(type: SdkType): MultiPartFileType { return { kind: "multipartfile", type }; } function addDisableGenerationMap(context: PythonSdkContext, type: SdkType): void { if (context.__disableGenerationMap.has(type)) return; context.__disableGenerationMap.add(type); if (type.kind === "model" && type.baseModel) { addDisableGenerationMap(context, type.baseModel); } else if (type.kind === "array") { addDisableGenerationMap(context, type.valueType); } } function emitProperty( context: PythonSdkContext, property: SdkModelPropertyType, ): Record { const isMultipartFileInput = property.serializationOptions?.multipart?.isFilePart; let sourceType: SdkType | MultiPartFileType = property.type; if (isMultipartFileInput) { sourceType = createMultiPartFileType(property.type); // Python convert all the type of file part to FileType so clear these models' usage so that they won't be generated addDisableGenerationMap(context, property.type); } const isNullable = !isMultipartFileInput && sourceType.kind === "nullable"; const booleanEncode = property.type.kind === "boolean" && property.__raw ? getEncode(context.program, property.__raw) : undefined; return { clientName: getClientName(property), isExactName: property.isExactName, wireName: (property.serializationOptions?.multipart ? property.serializationOptions?.multipart?.name : property.serializationOptions?.json?.name) ?? property.name, type: getType(context, sourceType), optional: property.optional, nullable: isNullable, description: property.summary ? property.summary : property.doc, addedOn: getAddedOn(context, property), apiVersions: property.apiVersions, visibility: visibilityMapping(property.visibility), isDiscriminator: property.discriminator, flatten: property.flatten, isMultipartFileInput: isMultipartFileInput, xmlMetadata: getXmlMetadata(property), encode: property.encode ?? (booleanEncode?.type.name === "string" ? "str" : undefined), clientDefaultValue: property.clientDefaultValue, }; } function emitModel(context: PythonSdkContext, type: SdkModelType): Record { if (isEmptyModel(type)) { return KnownTypes.any; } if (context.__typesMap.has(type)) { return context.__typesMap.get(type)!; } if (type.crossLanguageDefinitionId === "Azure.Core.Foundations.Error") { return { type: "sdkcore", name: "ODataV4Format", submodule: "exceptions", }; } if (type.crossLanguageDefinitionId === "Azure.Core.Foundations.ErrorResponse") { return { type: "sdkcore", name: "HttpResponseError", submodule: "exceptions", }; } if (type.external) { return getSimpleTypeResult(context, { type: "external", externalTypeInfo: type.external, }); } const parents: Record[] = []; const newValue = { type: type.kind, name: type.name, description: type.summary ? type.summary : type.doc, parents: parents, discriminatorValue: type.discriminatorValue, discriminatedSubtypes: {} as Record>, properties: new Array>(), snakeCaseName: camelToSnakeCase(type.name), base: "dpg", internal: type.access === "internal", crossLanguageDefinitionId: type.crossLanguageDefinitionId, usage: type.usage, isXml: type.usage & UsageFlags.Xml ? true : false, xmlMetadata: getXmlMetadata(type), clientNamespace: getClientNamespace(context, type.namespace), }; context.__typesMap.set(type, newValue); newValue.parents = type.baseModel ? [getType(context, type.baseModel)] : newValue.parents; for (const property of type.properties.values()) { if (property.kind === "property" && !isHttpMetadata(context, property)) { newValue.properties.push(emitProperty(context, property)); // type for base discriminator returned by TCGC changes from constant to string while // autorest treat all discriminator as constant type, so we need to change to constant type here if (type.discriminatedSubtypes && property.discriminator) { newValue.properties[newValue.properties.length - 1].isPolymorphic = true; if (property.type.kind === "string") { newValue.properties[newValue.properties.length - 1].type = getConstantType(context, null); } } } } if (type.discriminatedSubtypes) { for (const key in type.discriminatedSubtypes) { newValue.discriminatedSubtypes[key] = getType(context, type.discriminatedSubtypes[key]); } } return newValue; } function getConstantFromEnumValueType( context: PythonSdkContext, type: SdkEnumValueType, ): Record { return getSimpleTypeResult(context, { type: "constant", value: type.value, valueType: emitBuiltInType(context, type.valueType), }); } function emitEnum(context: PythonSdkContext, type: SdkEnumType): Record { if (context.__typesMap.has(type)) { return context.__typesMap.get(type)!; } if (type.isGeneratedName) { const types = []; for (const value of type.values) { types.push(getConstantFromEnumValueType(context, value)); } if (!type.isFixed) { types.push(emitBuiltInType(context, type.valueType)); } const newValue = { description: "", internal: true, type: "combined", types, xmlMetadata: {}, }; context.__typesMap.set(type, newValue); return newValue; } const values: Record[] = []; const name = type.name; const newValue = { name: name, snakeCaseName: camelToSnakeCase(name), description: (type.summary ? type.summary : type.doc) ?? `Type of ${name}`, internal: type.access === "internal", type: type.kind, valueType: emitBuiltInType(context, type.valueType), values, xmlMetadata: {}, crossLanguageDefinitionId: type.crossLanguageDefinitionId, clientNamespace: getClientNamespace(context, type.namespace), }; for (const value of type.values) { newValue.values.push(emitEnumMember(context, value, newValue)); } context.__typesMap.set(type, newValue); return newValue; } function enumName(name: string): string { return camelToSnakeCase(name).toUpperCase(); } function emitEnumMember( context: PythonSdkContext, type: SdkEnumValueType, enumType: Record, ): Record { if (context.__typesMap.has(type)) { return context.__typesMap.get(type)!; } // python don't generate enum created by TCGC, so we shall not generate type for enum member of the enum, either. if (type.enumType.isGeneratedName) { return getConstantFromEnumValueType(context, type); } const result = { name: type.isExactName ? type.name : enumName(type.name), isExactName: type.isExactName, value: type.value, description: type.summary ? type.summary : type.doc, enumType, type: type.kind, valueType: enumType["valueType"], }; context.__typesMap.set(type, result); return result; } function emitDurationOrDateType( context: PythonSdkContext, type: SdkDurationType | SdkDateTimeType, ): Record { return getSimpleTypeResult(context, { ...emitBuiltInType(context, type), wireType: emitBuiltInType(context, type.wireType), }); } function emitArrayOrDict( context: PythonSdkContext, type: SdkArrayType | SdkDictionaryType, ): Record { const kind = type.kind === "array" ? "list" : type.kind; return getSimpleTypeResult(context, { type: kind, elementType: getType(context, type.valueType), }); } function emitConstant(context: PythonSdkContext, type: SdkConstantType) { return getSimpleTypeResult(context, { type: type.kind, value: type.value, valueType: emitBuiltInType(context, type.valueType), }); } const sdkScalarKindToPythonKind: Record = { numeric: "float", integer: "integer", safeint: "integer", int8: "integer", uint8: "integer", int16: "integer", uint16: "integer", int32: "integer", uint32: "integer", int64: "integer", uint64: "integer", float: "float", float32: "float", float64: "float", decimal: "decimal", decimal128: "decimal", string: "string", password: "string", guid: "string", url: "string", uri: "string", uuid: "string", etag: "string", armId: "string", ipAddress: "string", azureLocation: "string", }; function emitBuiltInType( context: PythonSdkContext, type: SdkBuiltInType | SdkDurationType | SdkDateTimeType, ): Record { if (type.encode) { if (type.kind === "duration") { if (type.encode === "ISO8601") { return getSimpleTypeResult(context, { type: type.kind, encode: type.encode, }); } if (type.encode === "seconds" || type.encode === "milliseconds") { return getSimpleTypeResult(context, { type: type.kind, encode: type.encode, wireType: getType(context, type.wireType), }); } } if (type.kind === "utcDateTime" || type.kind === "offsetDateTime") { if (type.encode === "unixTimestamp") { return getSimpleTypeResult(context, { type: "unixtime", encode: type.encode, }); } if (type.encode === "rfc3339" || type.encode === "rfc7231") { return getSimpleTypeResult(context, { type: type.kind, encode: type.encode, }); } } // fallback to wire type for unknown or unsupported encode if ("wireType" in type && type.wireType !== undefined) { return getSimpleTypeResult(context, { type: sdkScalarKindToPythonKind[type.wireType.kind] || type.wireType.kind, encode: type.encode, }); } } return getSimpleTypeResult(context, { type: sdkScalarKindToPythonKind[type.kind] || type.kind, // TODO: switch to kind encode: type.encode, }); } function emitUnion(context: PythonSdkContext, type: SdkUnionType): Record { return getSimpleTypeResult(context, { name: type.isGeneratedName ? undefined : type.name, snakeCaseName: type.isGeneratedName ? undefined : camelToSnakeCase(type.name), description: type.isGeneratedName ? "" : `Type of ${type.name}`, internal: true, type: "combined", types: type.variantTypes.map((x) => getType(context, x)), xmlMetadata: {}, clientNamespace: getClientNamespace(context, type.namespace), }); } export function getConstantType( context: PythonSdkContext, key: string | null, ): Record { const cache = context.__simpleTypesMap.get(key); if (cache) { return cache; } const type = { apiVersions: [], type: "constant", value: key, valueType: KnownTypes.string, xmlMetadata: {}, }; context.__simpleTypesMap.set(key, type); return type; } export const KnownTypes = { string: { type: "string" }, anyObject: { type: "any-object" }, any: { type: "any" }, }; /** * Detect whether an endpoint template parameter should be treated as an API * version parameter even when TCGC does not flag it as such. This is a * compatibility shim for unreleased TCGC changes that gate the name-based * heuristic on `isMetadata`, which excludes server template parameters. */ function isEndpointApiVersionFallback( param: { name: string; isApiVersionParam: boolean }, serviceApiVersions: string[], ): boolean { if (param.isApiVersionParam) return false; if (serviceApiVersions.length === 0) return false; const lower = param.name.toLowerCase(); return lower === "apiversion" || lower === "api-version"; } export function emitEndpointType( context: PythonSdkContext, type: SdkEndpointType, serviceApiVersions: string[], ): Record[] { const params: Record[] = []; for (const param of type.templateArguments) { const paramBase = emitParamBase(context, param, undefined, serviceApiVersions); paramBase.clientName = context.arm ? "base_url" : paramBase.clientName; let effectiveClientDefaultValue = param.clientDefaultValue; // If this endpoint template param looks like an api-version but TCGC // did not flag it, apply fallback: mark as api version and derive defaults. if (isEndpointApiVersionFallback(param, serviceApiVersions)) { paramBase.isApiVersion = true; if (!effectiveClientDefaultValue) { effectiveClientDefaultValue = serviceApiVersions[serviceApiVersions.length - 1]; } paramBase.type = getSimpleTypeResult(context, { type: "constant", value: effectiveClientDefaultValue, valueType: paramBase.type, }); } params.push({ ...paramBase, optional: Boolean(effectiveClientDefaultValue), wireName: param.name, location: "endpointPath", implementation: getImplementation(context, param), clientDefaultValue: effectiveClientDefaultValue, skipUrlEncoding: param.allowReserved, }); context.__endpointPathParameters!.push(params.at(-1)!); } return params; } function getXmlMetadata( type: SdkModelType | SdkModelPropertyType, ): Record | undefined { if (type.serializationOptions.xml) { return { name: type.serializationOptions.xml.name, namespace: type.serializationOptions.xml.ns?.namespace, prefix: type.serializationOptions.xml.ns?.prefix, attribute: type.serializationOptions.xml.attribute, unwrapped: type.kind === "property" && type.type.kind === "array" && type.serializationOptions.xml.unwrapped, text: type.kind === "property" && type.type.kind !== "array" && type.serializationOptions.xml.unwrapped, itemsName: type.serializationOptions.xml.itemsName, itemsNs: type.serializationOptions.xml.itemsNs?.namespace, itemsPrefix: type.serializationOptions.xml.itemsNs?.prefix, }; } return undefined; }