Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4bd9edd
feat(python): upgrade to IR v66
Swimburger Apr 1, 2026
c95434e
chore: register Python generator versions in IR v66 migration mapping
Swimburger Apr 1, 2026
3068c87
chore: update seed.yml irVersion to v66 for Python generators
Swimburger Apr 1, 2026
6c974ff
feat(python): implement v66 IR NameOrString handling with CaseConverter
Swimburger Apr 2, 2026
b07c54d
fix(python): replace remaining key.wireValue direct accesses with get…
Swimburger Apr 2, 2026
edf9b04
Merge origin/main into python-ir-v66-upgrade - resolve versions.yml c…
Swimburger Apr 2, 2026
3dbbf49
fix(python): revert dynamic-snippets changes - dynamic IR does not us…
Swimburger Apr 2, 2026
063cdaa
fix(python): fix biome formatting issues in v66 changes
Swimburger Apr 2, 2026
fa1811e
fix(python): revert seed.yml irVersion and migration - Python IR SDK …
Swimburger Apr 2, 2026
5fc0d03
Merge remote-tracking branch 'origin/main' into devin/1775086696-pyth…
Swimburger Apr 2, 2026
b1eeddf
fix(python): pin IR version to v65 in test_ir_deserialization to matc…
Swimburger Apr 3, 2026
e7596a2
Merge remote-tracking branch 'origin/main' into devin/1775086696-pyth…
Swimburger Apr 3, 2026
69f4d58
Merge branch 'main' into devin/1775086696-python-ir-v66-upgrade
Swimburger Apr 3, 2026
dd38952
Merge remote-tracking branch 'origin/main' into devin/1775086696-pyth…
Swimburger Apr 4, 2026
3c21709
Merge remote-tracking branch 'origin/main' into devin/1775086696-pyth…
Swimburger Apr 6, 2026
87a352e
Merge remote-tracking branch 'origin/main' into devin/1775086696-pyth…
Swimburger Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { AbstractGeneratorContext, FernGeneratorExec, GeneratorNotificationService } from "@fern-api/base-generator";
import {
AbstractGeneratorContext,
CaseConverter,
FernGeneratorExec,
GeneratorNotificationService
} from "@fern-api/base-generator";
import { FernIr } from "@fern-fern/ir-sdk";
import { snakeCase } from "lodash-es";

import { BasePythonCustomConfigSchema } from "../custom-config/BasePythonCustomConfigSchema.js";
import { PythonProject } from "../project/index.js";
import { PythonTypeMapper } from "./PythonTypeMapper.js";

const caseConverter = new CaseConverter({ generationLanguage: "python", keywords: undefined, smartCasing: true });

export abstract class AbstractPythonGeneratorContext<
CustomConfig extends BasePythonCustomConfigSchema
> extends AbstractGeneratorContext {
Expand All @@ -20,7 +27,7 @@ export abstract class AbstractPythonGeneratorContext<
public readonly generatorNotificationService: GeneratorNotificationService
) {
super(config, generatorNotificationService);
this.packageName = snakeCase(`${this.config.organization}_${this.ir.apiName.snakeCase.unsafeName}`);
this.packageName = snakeCase(`${this.config.organization}_${caseConverter.snakeUnsafe(this.ir.apiName)}`);
this.pythonTypeMapper = new PythonTypeMapper(this);
this.project = new PythonProject({ context: this });
}
Expand All @@ -47,15 +54,15 @@ export abstract class AbstractPythonGeneratorContext<
}

public getClassName(name: FernIr.Name): string {
return name.pascalCase.safeName;
return caseConverter.pascalSafe(name);
}

public getPascalCaseSafeName(name: FernIr.Name): string {
return name.pascalCase.safeName;
return caseConverter.pascalSafe(name);
}

public getSnakeCaseSafeName(name: FernIr.Name): string {
return name.snakeCase.safeName;
return caseConverter.snakeSafe(name);
}

public getModulePathForId(typeId: string): string[] {
Expand Down
13 changes: 8 additions & 5 deletions generators/python-v2/pydantic-model/src/v2/EnumGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { CaseConverter, getWireValue } from "@fern-api/base-generator";
import { RelativeFilePath } from "@fern-api/fs-utils";
import { python } from "@fern-api/python-ast";
import { WriteablePythonFile } from "@fern-api/python-base";
import { FernIr } from "@fern-fern/ir-sdk";

import { PydanticModelGeneratorContext } from "../ModelGeneratorContext.js";

const caseConverter = new CaseConverter({ generationLanguage: "python", keywords: undefined, smartCasing: true });

export class EnumGenerator {
constructor(
private readonly typeId: FernIr.TypeId,
Expand All @@ -25,8 +28,8 @@ export class EnumGenerator {

// Add enum members
for (const enumValue of this.enumDeclaration.values) {
const memberName = enumValue.name.name.screamingSnakeCase.safeName;
const wireValue = this.escapeStringForPython(enumValue.name.wireValue);
const memberName = caseConverter.screamingSnakeSafe(enumValue.name.name);
const wireValue = this.escapeStringForPython(getWireValue(enumValue.name));

enumClass.addField(
python.field({
Expand Down Expand Up @@ -65,7 +68,7 @@ export class EnumGenerator {
type: undefined
}),
...this.enumDeclaration.values.map((enumValue) => {
const parameterName = enumValue.name.name.snakeCase.safeName;
const parameterName = caseConverter.snakeSafe(enumValue.name.name);
return python.parameter({
name: parameterName,
type: python.Type.reference(
Expand All @@ -85,8 +88,8 @@ export class EnumGenerator {

// Add if statements for each enum value
for (const enumValue of this.enumDeclaration.values) {
const memberName = enumValue.name.name.screamingSnakeCase.safeName;
const parameterName = enumValue.name.name.snakeCase.safeName;
const memberName = caseConverter.screamingSnakeSafe(enumValue.name.name);
const parameterName = caseConverter.snakeSafe(enumValue.name.name);

visitMethod.addStatement(
python.codeBlock((writer) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getWireValue } from "@fern-api/base-generator";
import { RelativeFilePath } from "@fern-api/fs-utils";
import { python } from "@fern-api/python-ast";
import { core, dt, pydantic, WriteablePythonFile } from "@fern-api/python-base";
Expand Down Expand Up @@ -41,7 +42,7 @@ export class ObjectGenerator {
? python.codeBlock("None")
: undefined;

const wireValue = propertyName === property.name.wireValue ? undefined : property.name.wireValue;
const wireValue = propertyName === getWireValue(property.name) ? undefined : getWireValue(property.name);

let initializer = undefined;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CaseConverter } from "@fern-api/base-generator";
import { assertNever } from "@fern-api/core-utils";
import { RelativeFilePath } from "@fern-api/fs-utils";
import { python } from "@fern-api/python-ast";
Expand All @@ -6,6 +7,8 @@ import { FernIr } from "@fern-fern/ir-sdk";

import { PydanticModelGeneratorContext } from "../ModelGeneratorContext.js";

const caseConverter = new CaseConverter({ generationLanguage: "python", keywords: undefined, smartCasing: true });

export class WrappedAliasGenerator {
private readonly className: string;

Expand Down Expand Up @@ -110,7 +113,7 @@ export class WrappedAliasGenerator {
literal: () => "get_as_string",
_other: () => "get_value"
}),
named: (typeName) => "get_as_" + typeName.name.snakeCase.unsafeName,
named: (typeName) => "get_as_" + caseConverter.snakeUnsafe(typeName.name),
primitive: (primitive) => {
if (primitive.v2 != null) {
return primitive.v2?._visit({
Expand Down Expand Up @@ -202,7 +205,7 @@ export class WrappedAliasGenerator {
literal: () => "from_string",
_other: () => "from_value"
}),
named: (typeName) => "from_" + typeName.name.snakeCase.unsafeName,
named: (typeName) => "from_" + caseConverter.snakeUnsafe(typeName.name),
primitive: (primitive) => {
if (primitive.v2 != null) {
return primitive.v2?._visit({
Expand Down
36 changes: 21 additions & 15 deletions generators/python-v2/sdk/src/readme/ReadmeSnippetBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { AbstractReadmeSnippetBuilder } from "@fern-api/base-generator";
import { AbstractReadmeSnippetBuilder, CaseConverter } from "@fern-api/base-generator";
import { FernGeneratorCli } from "@fern-fern/generator-cli-sdk";
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
import { FernIr } from "@fern-fern/ir-sdk";

import { SdkGeneratorContext } from "../SdkGeneratorContext.js";

const caseConverter = new CaseConverter({ generationLanguage: "python", keywords: undefined, smartCasing: true });

interface EndpointWithFilepath {
endpoint: FernIr.HttpEndpoint;
fernFilepath: FernIr.FernFilepath;
Expand Down Expand Up @@ -291,20 +293,20 @@ asyncio.run(main())`
switch (scheme.type) {
case "bearer":
args.push(
` ${scheme.token.snakeCase.unsafeName}="YOUR_${scheme.token.screamingSnakeCase.unsafeName}",`
` ${caseConverter.snakeUnsafe(scheme.token)}="YOUR_${caseConverter.screamingSnakeUnsafe(scheme.token)}",`
);
break;
case "basic":
args.push(
` ${scheme.username.snakeCase.unsafeName}="YOUR_${scheme.username.screamingSnakeCase.unsafeName}",`
` ${caseConverter.snakeUnsafe(scheme.username)}="YOUR_${caseConverter.screamingSnakeUnsafe(scheme.username)}",`
);
args.push(
` ${scheme.password.snakeCase.unsafeName}="YOUR_${scheme.password.screamingSnakeCase.unsafeName}",`
` ${caseConverter.snakeUnsafe(scheme.password)}="YOUR_${caseConverter.screamingSnakeUnsafe(scheme.password)}",`
);
break;
case "header": {
const headerName = scheme.name.name.snakeCase.unsafeName;
const headerScreaming = scheme.name.name.screamingSnakeCase.unsafeName;
const headerName = caseConverter.snakeUnsafe(scheme.name.name);
const headerScreaming = caseConverter.screamingSnakeUnsafe(scheme.name.name);
args.push(` ${headerName}="YOUR_${headerScreaming}",`);
break;
}
Expand Down Expand Up @@ -488,7 +490,7 @@ print(response.data) # access the underlying object`
}

const { subpackage, channel } = websocketInfo;
const subpackageName = subpackage.name.snakeCase.safeName;
const subpackageName = caseConverter.snakeSafe(subpackage.name);
// connectMethodName may not exist on older IR SDK versions
const connectMethodName = (channel as unknown as { connectMethodName?: string }).connectMethodName;
const connectMethodNameSnakeCase = this.toSnakeCase(connectMethodName ?? "connect");
Expand Down Expand Up @@ -674,20 +676,24 @@ ${constructorArg}
const envs = envConfig.environments.environments;
if (defaultEnvId != null) {
const defaultEnv = envs.find((e) => e.id === defaultEnvId);
firstEnvName = defaultEnv?.name.screamingSnakeCase.unsafeName;
if (defaultEnv?.name != null) {
firstEnvName = caseConverter.screamingSnakeUnsafe(defaultEnv.name);
}
}
if (firstEnvName == null && envs.length > 0 && envs[0] != null) {
firstEnvName = envs[0].name.screamingSnakeCase.unsafeName;
firstEnvName = caseConverter.screamingSnakeUnsafe(envs[0].name);
}
} else if (envConfig.environments.type === "multipleBaseUrls") {
const defaultEnvId = envConfig.defaultEnvironment;
const envs = envConfig.environments.environments;
if (defaultEnvId != null) {
const defaultEnv = envs.find((e) => e.id === defaultEnvId);
firstEnvName = defaultEnv?.name.screamingSnakeCase.unsafeName;
if (defaultEnv?.name != null) {
firstEnvName = caseConverter.screamingSnakeUnsafe(defaultEnv.name);
}
}
if (firstEnvName == null && envs.length > 0 && envs[0] != null) {
firstEnvName = envs[0].name.screamingSnakeCase.unsafeName;
firstEnvName = caseConverter.screamingSnakeUnsafe(envs[0].name);
}
}

Expand Down Expand Up @@ -722,14 +728,14 @@ ${constructorArg}
}

private getEndpointAccessPath(endpoint: EndpointWithFilepath): string {
const clientAccessParts = endpoint.fernFilepath.allParts.map((part) => part.snakeCase.safeName);
const methodName = endpoint.endpoint.name.snakeCase.unsafeName;
const clientAccessParts = endpoint.fernFilepath.allParts.map((part) => caseConverter.snakeSafe(part));
const methodName = caseConverter.snakeUnsafe(endpoint.endpoint.name);
return clientAccessParts.length > 0 ? `${clientAccessParts.join(".")}.${methodName}` : methodName;
}

private getRawResponseMethodCall(endpoint: EndpointWithFilepath): string {
const clientAccessParts = endpoint.fernFilepath.allParts.map((part) => part.snakeCase.safeName);
const methodName = endpoint.endpoint.name.snakeCase.unsafeName;
const clientAccessParts = endpoint.fernFilepath.allParts.map((part) => caseConverter.snakeSafe(part));
const methodName = caseConverter.snakeUnsafe(endpoint.endpoint.name);
if (clientAccessParts.length > 0) {
return `client.${clientAccessParts.join(".")}.with_raw_response.${methodName}`;
}
Expand Down
43 changes: 26 additions & 17 deletions generators/python-v2/sdk/src/reference/buildReference.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ReferenceConfigBuilder } from "@fern-api/base-generator";
import { CaseConverter, ReferenceConfigBuilder } from "@fern-api/base-generator";
import { FernGeneratorCli } from "@fern-fern/generator-cli-sdk";
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
import { FernIr } from "@fern-fern/ir-sdk";
import { SdkGeneratorContext } from "../SdkGeneratorContext.js";

const caseConverter = new CaseConverter({ generationLanguage: "python", keywords: undefined, smartCasing: true });

export function buildReference({
context,
endpointSnippets
Expand Down Expand Up @@ -87,7 +89,7 @@ function getEndpointReference({
endpoint: FernIr.HttpEndpoint;
endpointSnippets: Record<string, string>;
}): FernGeneratorCli.EndpointReference | undefined {
const methodName = endpoint.name.snakeCase.unsafeName;
const methodName = caseConverter.snakeUnsafe(endpoint.name);
const accessPath = getAccessFromRootClient({ service });
const parameters = getEndpointParameters({ endpoint });
const sourceFilePath = getSourceFilePath({ context, service });
Expand Down Expand Up @@ -133,7 +135,7 @@ function getEndpointReference({

function getAccessFromRootClient({ service }: { service: FernIr.HttpService }): string {
const clientVariableName = "client";
const servicePath = service.name.fernFilepath.allParts.map((part) => part.snakeCase.safeName);
const servicePath = service.name.fernFilepath.allParts.map((part) => caseConverter.snakeSafe(part));
return servicePath.length > 0 ? `${clientVariableName}.${servicePath.join(".")}` : clientVariableName;
}

Expand All @@ -142,7 +144,7 @@ function getEndpointParameters({ endpoint }: { endpoint: FernIr.HttpEndpoint }):

endpoint.allPathParameters.forEach((pathParam) => {
parameters.push({
name: pathParam.name.snakeCase.unsafeName,
name: caseConverter.snakeUnsafe(pathParam.name),
type: getTypeString(pathParam.valueType),
description: pathParam.docs,
required: !isTypeOptional(pathParam.valueType)
Expand All @@ -162,7 +164,7 @@ function getEndpointParameters({ endpoint }: { endpoint: FernIr.HttpEndpoint }):
type = getTypeString(queryParam.valueType);
}
parameters.push({
name: queryParam.name.name.snakeCase.unsafeName,
name: caseConverter.snakeUnsafe(queryParam.name.name),
type,
description: queryParam.docs,
required: !isOptional
Expand All @@ -171,7 +173,7 @@ function getEndpointParameters({ endpoint }: { endpoint: FernIr.HttpEndpoint }):

endpoint.headers.forEach((header) => {
parameters.push({
name: header.name.name.snakeCase.unsafeName,
name: caseConverter.snakeUnsafe(header.name.name),
type: getTypeString(header.valueType),
description: header.docs,
required: !isTypeOptional(header.valueType)
Expand All @@ -183,7 +185,7 @@ function getEndpointParameters({ endpoint }: { endpoint: FernIr.HttpEndpoint }):
if (endpoint.requestBody.extendedProperties != null) {
endpoint.requestBody.extendedProperties.forEach((property) => {
parameters.push({
name: property.name.name.snakeCase.unsafeName,
name: caseConverter.snakeUnsafe(property.name.name),
type: getTypeString(property.valueType),
description: property.docs,
required: !isTypeOptional(property.valueType)
Expand All @@ -192,7 +194,7 @@ function getEndpointParameters({ endpoint }: { endpoint: FernIr.HttpEndpoint }):
}
endpoint.requestBody.properties.forEach((property) => {
parameters.push({
name: property.name.name.snakeCase.unsafeName,
name: caseConverter.snakeUnsafe(property.name.name),
type: getTypeString(property.valueType),
description: property.docs,
required: !isTypeOptional(property.valueType)
Expand All @@ -213,14 +215,14 @@ function getEndpointParameters({ endpoint }: { endpoint: FernIr.HttpEndpoint }):
const fileType = fileProperty.type === "fileArray" ? "typing.List[core.File]" : "core.File";
const type = isOptional ? `typing.Optional[${fileType}]` : fileType;
parameters.push({
name: fileProperty.key.name.snakeCase.unsafeName,
name: caseConverter.snakeUnsafe(fileProperty.key.name),
type,
description: fileProperty.docs,
required: !isOptional
});
} else if (property.type === "bodyProperty") {
parameters.push({
name: property.name.name.snakeCase.unsafeName,
name: caseConverter.snakeUnsafe(property.name.name),
type: getTypeString(property.valueType),
description: property.docs,
required: !isTypeOptional(property.valueType)
Expand Down Expand Up @@ -302,7 +304,7 @@ function getTypeString(typeReference: FernIr.TypeReference): string {
}
}
case "named":
return typeReference.name.pascalCase.unsafeName;
return caseConverter.pascalUnsafe(typeReference.name);
case "unknown":
return "typing.Any";
default:
Expand All @@ -318,7 +320,7 @@ function getSourceFilePath({
service: FernIr.HttpService;
}): string | undefined {
const modulePath = context.getModulePath().replace(/-/g, "_");
const pathParts = service.name.fernFilepath.allParts.map((part) => part.snakeCase.safeName);
const pathParts = service.name.fernFilepath.allParts.map((part) => caseConverter.snakeSafe(part));
if (pathParts.length === 0) {
return `src/${modulePath}/client.py`;
}
Expand Down Expand Up @@ -359,7 +361,10 @@ function isRootServiceId({
}

function getSectionTitle({ service }: { service: FernIr.HttpService }): string {
return service.displayName ?? service.name.fernFilepath.allParts.map((part) => part.pascalCase.safeName).join(" ");
return (
service.displayName ??
service.name.fernFilepath.allParts.map((part) => caseConverter.pascalSafe(part)).join(" ")
);
}

function isTypeOptional(typeReference: FernIr.TypeReference): boolean {
Expand Down Expand Up @@ -491,20 +496,24 @@ function getEnvironmentInfo({
const envs = envConfig.environments.environments;
if (defaultEnvId != null) {
const defaultEnv = envs.find((e) => e.id === defaultEnvId);
firstEnvName = defaultEnv?.name.screamingSnakeCase.unsafeName;
if (defaultEnv?.name != null) {
firstEnvName = caseConverter.screamingSnakeUnsafe(defaultEnv.name);
}
}
if (firstEnvName == null && envs.length > 0 && envs[0] != null) {
firstEnvName = envs[0].name.screamingSnakeCase.unsafeName;
firstEnvName = caseConverter.screamingSnakeUnsafe(envs[0].name);
}
} else if (envConfig.environments.type === "multipleBaseUrls") {
const defaultEnvId = envConfig.defaultEnvironment;
const envs = envConfig.environments.environments;
if (defaultEnvId != null) {
const defaultEnv = envs.find((e) => e.id === defaultEnvId);
firstEnvName = defaultEnv?.name.screamingSnakeCase.unsafeName;
if (defaultEnv?.name != null) {
firstEnvName = caseConverter.screamingSnakeUnsafe(defaultEnv.name);
}
}
if (firstEnvName == null && envs.length > 0 && envs[0] != null) {
firstEnvName = envs[0].name.screamingSnakeCase.unsafeName;
firstEnvName = caseConverter.screamingSnakeUnsafe(envs[0].name);
}
}

Expand Down
Loading
Loading