Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7f8ee05
feat(csharp): Add support for gRPC bytes and nested enums (#13572)
amckinney Mar 16, 2026
4bad41b
fix(typescript): Remove "baseUrl" configuration from multiple TypeScr…
fern-support Mar 16, 2026
05cdce4
fix(go): Basic Auth header not sent when username is empty (#13563)
iamnamananand996 Mar 16, 2026
8cda669
chore(go): update go-sdk seed (#13576)
fern-support Mar 16, 2026
9760e31
fix(python): wrap python-version in quotes in generated CI workflow (…
jsklan Mar 16, 2026
605795e
fix(openapi): disambiguate streaming request wrapper name for stream-…
Swimburger Mar 16, 2026
f0d9b3a
chore(python): update pydantic seed (#13578)
fern-support Mar 16, 2026
cf8a3ef
chore(python): update python-sdk seed (#13579)
fern-support Mar 16, 2026
884a18b
chore(python): update fastapi seed (#13580)
fern-support Mar 16, 2026
4a4e48b
fix(cli): Add support for x-fern-encoding on arbitrary objects (#13583)
amckinney Mar 16, 2026
03f673b
feat(python): raise default minimum Python version from ^3.8 to ^3.10…
jsklan Mar 16, 2026
ed9b7a0
chore(python): update pydantic seed (#13590)
fern-support Mar 16, 2026
b02499c
chore(python): update fastapi seed (#13581)
fern-support Mar 16, 2026
70ec403
feat(cli): Infer gRPC endpoints without HTTP annotations (#13583) (#1…
amckinney Mar 16, 2026
81044d3
feat(typescript): expose `protocols` field on WebSocket ConnectArgs (…
Swimburger Mar 16, 2026
146403c
chore(python): upgrade pytest-asyncio from ^0.23.5 to ^1.0.0 in gener…
jsklan Mar 16, 2026
234a7fe
chore(python): update pydantic seed (#13596)
fern-support Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 6 additions & 1 deletion generators/csharp/base/src/context/CsharpTypeMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,12 @@ export class CsharpTypeMapper extends WithGeneration {
dateTime: () => this.Value.dateTime,
uuid: () => this.Primitive.string,
// https://learn.microsoft.com/en-us/dotnet/api/system.convert.tobase64string?view=net-8.0
base64: () => this.Primitive.string,
//
// TODO: The protoc-gen-openapi plugin represents bytes as base64 properties. For this to
// be correct, we need a bytes primitive type in the IR. For now, this is only an issue in
// rare cases, where the SDK requires both gRPC and REST endpoints with base64 and byte[]
// properties.
base64: () => (this.context.hasGrpcEndpoints() ? this.Value.binary : this.Primitive.string),
bigInteger: () => this.Primitive.string,
dateTimeRfc2822: () => this.Value.dateTime,
_other: () => this.Primitive.object
Expand Down
159 changes: 127 additions & 32 deletions generators/csharp/base/src/proto/CsharpProtobufTypeMapper.ts

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions generators/csharp/base/src/proto/ProtobufResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,16 @@ export class ProtobufResolver extends WithGeneration {
.filter((segment) => !this.namespaces.root.split(".").includes(segment))
.join("_")
);
// Nested proto types use underscore-separated names (e.g. "InvoiceBundle_Status")
// from protoc-gen-openapi. In C# protobuf codegen, nested types are
// accessed as "ParentMessage.Types.NestedType".
const originalName = protobufType.name.originalName;
const protoClassName = originalName.includes("_")
? originalName.split("_").join(".Types.")
: originalName;
return this.csharp.classReference({
name: protobufType.name.originalName,
namespace: this.context.protobufResolver.getNamespaceFromProtobufFile(protobufType.file),
name: protoClassName,
namespace: protoNamespace,
namespaceAlias: `Proto${aliasSuffix.charAt(0).toUpperCase()}${aliasSuffix.slice(1)}`
});
}
Expand Down
9 changes: 9 additions & 0 deletions generators/csharp/codegen/src/context/extern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,15 @@ export class Extern {
*/
Protobuf: () =>
lazy({
/**
* Reference to Google.Protobuf.ByteString class.
*/
ByteString: () =>
this.csharp.classReference({
name: "ByteString",
namespace: "Google.Protobuf"
}),

/**
* Well-known types namespace references with namespace alias.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkGenera
);
const protobufProperties: {
propertyName: string;
protoPropertyName?: string;
typeReference: TypeReference;
}[] = [];

Expand Down Expand Up @@ -135,6 +136,7 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkGenera
if (isProtoRequest) {
protobufProperties.push({
propertyName: field.name,
protoPropertyName: query.name.name.pascalCase.safeName,
typeReference: query.allowMultiple
? FernIr.TypeReference.container(FernIr.ContainerType.list(query.valueType))
: query.valueType
Expand Down Expand Up @@ -196,6 +198,7 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkGenera
if (isProtoRequest) {
protobufProperties.push({
propertyName: field.name,
protoPropertyName: property.name.name.pascalCase.safeName,
typeReference: property.valueType
});
}
Expand Down
12 changes: 12 additions & 0 deletions generators/csharp/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 2.31.0
changelogEntry:
- summary: |
Add support for `base64` primitive type in the IR to represent Protobuf byte[].
type: feat
- summary: |
Fix the `.ToProto` and `.FromProto` mapper methods to support for enums nested within
message descriptors.
type: fix
createdAt: "2026-03-13"
irVersion: 65

- version: 2.30.3
changelogEntry:
- summary: |
Expand Down
2 changes: 1 addition & 1 deletion generators/go/internal/generator/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ func (f *fileWriter) WriteRequestOptionsDefinition(
username = authScheme.Basic.Username.PascalCase.UnsafeName
password = authScheme.Basic.Password.PascalCase.UnsafeName
)
f.P(`if r.`, username, ` != "" && r.`, password, ` != "" {`)
f.P(`if r.`, username, ` != "" || r.`, password, ` != "" {`)
f.P(`header.Set("Authorization", `, `"Basic " + base64.StdEncoding.EncodeToString([]byte(r.`, username, ` + ":" + r.`, password, `)))`)
f.P("}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ func NewRequestOptions(opts ...RequestOption) *RequestOptions {
// for the request(s).
func (r *RequestOptions) ToHeader() http.Header {
header := r.cloneHeader()
if r.Username != "" && r.Password != "" {
header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(r.Username+": "+r.Password)))
if r.Username != "" || r.Password != "" {
header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(r.Username+":"+r.Password)))
}
return header
}
Expand Down
12 changes: 12 additions & 0 deletions generators/go/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 1.30.2
changelogEntry:
- summary: |
Fix Basic Auth header not being sent when the username is an empty string.
Per RFC 7617, an empty user-id is valid in Basic Auth (e.g., `:password`).
The condition in `ToHeader()` now uses `||` instead of `&&`, so the
Authorization header is set whenever either the username or password is
non-empty.
type: fix
createdAt: "2026-03-16"
irVersion: 61

- version: 1.30.1
changelogEntry:
- summary: |
Expand Down
31 changes: 31 additions & 0 deletions generators/python/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
# For unreleased changes, use unreleased.yml
- version: 5.0.1
changelogEntry:
- summary: |
Upgrade `pytest-asyncio` dev dependency from `^0.23.5` to `^1.0.0` in generated
`pyproject.toml`. This eliminates deprecation warnings about `asyncio.iscoroutinefunction`,
`asyncio.get_event_loop_policy`, and `asyncio.set_event_loop_policy` when running tests
on Python 3.14+.
type: chore
createdAt: "2026-03-16"
irVersion: 65

- version: 5.0.0
changelogEntry:
- summary: |
Raise the default minimum Python version from `^3.8` to `^3.10` in generated
`pyproject.toml`. This allows the dependency resolver to pick Pydantic 2.11+ and
pydantic-core 2.33+, which ship pre-built wheels for Python 3.14, fixing
installation failures on Python 3.14.
type: feat
createdAt: "2026-03-16"
irVersion: 65

- version: 4.64.1
changelogEntry:
- summary: |
Wrap `python-version` in quotes in generated GitHub CI workflow so versions like
`^3.10` are not misinterpreted as `3.1` by the YAML parser.
type: fix
createdAt: "2026-03-16"
irVersion: 65

- version: 4.64.0
changelogEntry:
- summary: |
Expand Down
14 changes: 7 additions & 7 deletions generators/python/src/fern_python/cli/abstract_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def generate_project(
update={"installation_token": ir_publish_config.token}
)

python_version = "^3.8"
python_version = "^3.10"
if generator_config.custom_config is not None and "pyproject_python_version" in generator_config.custom_config:
python_version = generator_config.custom_config.get("pyproject_python_version")

Expand Down Expand Up @@ -351,7 +351,7 @@ def _get_github_workflow_legacy(
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: {ci_python_version}
python-version: "{ci_python_version}"
- name: Bootstrap poetry
run: |
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
Expand All @@ -367,7 +367,7 @@ def _get_github_workflow_legacy(
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: {ci_python_version}
python-version: "{ci_python_version}"
- name: Bootstrap poetry
run: |
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
Expand Down Expand Up @@ -406,7 +406,7 @@ def _get_github_workflow_legacy(
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: {ci_python_version}
python-version: "{ci_python_version}"
- name: Bootstrap poetry
run: |
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
Expand Down Expand Up @@ -443,7 +443,7 @@ def _get_github_workflow(
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: {ci_python_version}
python-version: "{ci_python_version}"
- name: Bootstrap poetry
run: |
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
Expand All @@ -459,7 +459,7 @@ def _get_github_workflow(
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: {ci_python_version}
python-version: "{ci_python_version}"
- name: Bootstrap poetry
run: |
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
Expand Down Expand Up @@ -497,7 +497,7 @@ def _get_github_workflow(
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: {ci_python_version}
python-version: "{ci_python_version}"
- name: Bootstrap poetry
run: |
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
Expand Down
2 changes: 1 addition & 1 deletion generators/python/src/fern_python/codegen/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(
filepath: str,
relative_path_to_project: str,
package_path: Optional[str] = None,
python_version: str = "^3.8",
python_version: str = "^3.10",
project_config: Optional[ProjectConfig] = None,
sorted_modules: Optional[Sequence[str]] = None,
flat_layout: bool = False,
Expand Down
23 changes: 21 additions & 2 deletions generators/python/src/fern_python/codegen/pyproject_toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import os
import re
import typing
from abc import ABC, abstractmethod
from dataclasses import dataclass
Expand Down Expand Up @@ -225,14 +226,32 @@ def to_string(self) -> str:
if self.enable_wire_tests:
wire_test_deps = 'requests = "^2.31.0"\ntypes-requests = "^2.31.0"\n'

# pytest-asyncio ^1.0.0 fixes Python 3.14+ deprecation warnings but
# requires pytest >= 8.2 and Python >= 3.9. Fall back to the older
# pair when the project still supports Python 3.8.
#
# Extract the minimum minor version from the constraint string
# (e.g. "^3.8" -> 8, "^3.8.1" -> 8, "^3.10" -> 10, ">=3.9" -> 9).
min_minor = 0
match = re.search(r"(\d+)\.(\d+)", self.python_version)
if match:
min_minor = int(match.group(2))

if min_minor >= 9:
pytest_version = "^8.2.0"
pytest_asyncio_version = "^1.0.0"
else:
pytest_version = "^7.4.0"
pytest_asyncio_version = "^0.23.5"

return f"""
[tool.poetry.dependencies]
python = "{self.python_version}"
{deps}
[tool.poetry.group.dev.dependencies]
mypy = "==1.13.0"
pytest = "^7.4.0"
pytest-asyncio = "^0.23.5"
pytest = "{pytest_version}"
pytest-asyncio = "{pytest_asyncio_version}"
pytest-xdist = "^3.6.1"
python-dateutil = "^2.9.0"
types-python-dateutil = "^2.9.0.20240316"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class SDKCustomConfig(pydantic.BaseModel):
# WARNING - this changes your declared python dependency, which is not meant to
# be done often if at all. This is a last resort if any dependencies force you
# to change your version requirements.
pyproject_python_version: Optional[str] = "^3.8"
pyproject_python_version: Optional[str] = "^3.10"

# Whether or not to generate TypedDicts instead of Pydantic
# Models for request objects.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@


class TestGithubCIPythonVersionResolver:
def test_resolve_default_constraint_returns_3_9(self) -> None:
"""Default pyproject_python_version (^3.8) should resolve to 3.9 for CI."""
default_python_version_constraint = "^3.8"
def test_resolve_default_constraint_returns_3_10(self) -> None:
"""Default pyproject_python_version (^3.10) should resolve to 3.10 for CI."""
default_python_version_constraint = "^3.10"
result = GithubCIPythonVersionResolver.resolve(default_python_version_constraint)
assert result == PythonVersion.PY3_9
assert result == PythonVersion.PY3_10

def test_resolve_uses_minimum_when_3_9_not_in_intersection(self) -> None:
result = GithubCIPythonVersionResolver.resolve("^3.10")
Expand Down
7 changes: 1 addition & 6 deletions generators/python/tests/codegen/test_python_version_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,8 @@ def test_impossible_constraint_returns_empty(self) -> None:
class TestGetMinimumCompatibleVersion:
"""Tests for get_minimum_compatible_version function."""

def test_caret_3_8_returns_3_8(self) -> None:
"""Test ^3.8 returns 3.8 as minimum."""
min_version = get_minimum_compatible_version("^3.8")
assert min_version == PythonVersion.PY3_8

def test_caret_3_10_returns_3_10(self) -> None:
"""Test ^3.10 returns 3.10 as minimum."""
"""Test ^3.10 (the default) returns 3.10 as minimum."""
min_version = get_minimum_compatible_version("^3.10")
assert min_version == PythonVersion.PY3_10

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class GeneratedDefaultWebsocketImplementation implements GeneratedWebsock
private static readonly HEADERS_PROPERTY_NAME = "headers";
private static readonly HEADERS_VARIABLE_NAME = "_headers";

private static readonly PROTOCOLS_PROPERTY_NAME = "protocols";
private static readonly RECONNECT_ATTEMPTS_PROPERTY_NAME = "reconnectAttempts";
private static readonly CONNECTION_TIMEOUT_PROPERTY_NAME = "connectionTimeoutInSeconds";
private static readonly ABORT_SIGNAL_PROPERTY_NAME = "abortSignal";
Expand Down Expand Up @@ -188,6 +189,19 @@ export class GeneratedDefaultWebsocketImplementation implements GeneratedWebsock
hasQuestionToken: context.type.isOptional(header.valueType)
};
}),
{
name: GeneratedDefaultWebsocketImplementation.PROTOCOLS_PROPERTY_NAME,
type: getTextOfTsNode(
ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ts.factory.createArrayTypeNode(
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
)
])
),
hasQuestionToken: true,
docs: ["WebSocket subprotocols to use for the connection."]
},
{
name: GeneratedDefaultWebsocketImplementation.ADDITIONAL_QUERY_PARAMETERS_PROPERTY_NAME,
type: getTextOfTsNode(
Expand Down Expand Up @@ -302,6 +316,15 @@ export class GeneratedDefaultWebsocketImplementation implements GeneratedWebsock
);
}

// Add protocols binding
bindingElements.push(
ts.factory.createBindingElement(
undefined,
undefined,
ts.factory.createIdentifier(GeneratedDefaultWebsocketImplementation.PROTOCOLS_PROPERTY_NAME)
)
);

// Add additional query parameters binding
bindingElements.push(
ts.factory.createBindingElement(
Expand Down Expand Up @@ -534,7 +557,11 @@ export class GeneratedDefaultWebsocketImplementation implements GeneratedWebsock

return context.coreUtilities.websocket.ReconnectingWebSocket._connect({
url: context.coreUtilities.urlUtils.join._invoke([baseUrl, url]),
protocols: ts.factory.createArrayLiteralExpression([]),
protocols: ts.factory.createBinaryExpression(
ts.factory.createIdentifier(GeneratedDefaultWebsocketImplementation.PROTOCOLS_PROPERTY_NAME),
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
ts.factory.createArrayLiteralExpression([])
),
options: ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
"debug",
Expand Down
Loading
Loading