Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -42,6 +42,7 @@
import com.palantir.conjure.python.processors.typename.PackagePrependingTypeNameProcessor;
import com.palantir.conjure.python.processors.typename.TypeNameProcessor;
import com.palantir.conjure.python.types.DefinitionImportTypeDefinitionVisitor;
import com.palantir.conjure.python.types.PythonErrorGenerator;
import com.palantir.conjure.python.types.PythonTypeGenerator;
import com.palantir.conjure.spec.ConjureDefinition;
import com.palantir.conjure.spec.TypeName;
Expand Down Expand Up @@ -146,6 +147,12 @@ private PythonFile getImplPythonFile(
definitionPackageNameProcessor,
definitionTypeNameProcessor,
dealiasingTypeVisitor);
PythonErrorGenerator errorGenerator = new PythonErrorGenerator(
implPackageNameProcessor,
implTypeNameProcessor,
definitionPackageNameProcessor,
definitionTypeNameProcessor,
dealiasingTypeVisitor);

List<PythonSnippet> snippets = new ArrayList<>();
snippets.addAll(conjureDefinition.getTypes().stream()
Expand All @@ -154,6 +161,11 @@ private PythonFile getImplPythonFile(
snippets.addAll(conjureDefinition.getServices().stream()
.map(clientGenerator::generateClient)
.toList());
if (config.generateErrorTypes()) {
snippets.addAll(conjureDefinition.getErrors().stream()
.map(errorGenerator::generateError)
.toList());
}

Map<PythonPackage, List<PythonSnippet>> snippetsByPackage =
snippets.stream().collect(Collectors.groupingBy(PythonSnippet::pythonPackage));
Expand Down Expand Up @@ -203,6 +215,19 @@ private List<PythonFile> getInitFiles(
importsByPackage.put(pythonPackage, pythonImport);
});

if (config.generateErrorTypes()) {
conjureDefinition.getErrors().forEach(errorDefinition -> {
PythonPackage pythonPackage = PythonPackage.of(definitionPackageNameProcessor.process(
errorDefinition.getErrorName().getPackage()));
PythonImport pythonImport = PythonImport.of(
moduleSpecifier,
NamedImport.of(
implTypeNameProcessor.process(errorDefinition.getErrorName()),
definitionTypeNameProcessor.process(errorDefinition.getErrorName())));
importsByPackage.put(pythonPackage, pythonImport);
});
}

return KeyedStream.stream(importsByPackage.build().asMap())
.map((pythonPackage, imports) -> {
List<String> importNames = imports.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public interface GeneratorConfiguration {

boolean generateRawSource();

@Value.Default
default boolean generateErrorTypes() {
return false;
}

default Optional<String> pythonicPackageName() {
return packageName().map(packageName -> packageName.replace('-', '_'));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.conjure.python.poet;

import com.google.common.collect.ImmutableList;
import com.palantir.conjure.python.processors.PythonIdentifierSanitizer;
import com.palantir.conjure.python.types.ImportTypeVisitor;
import com.palantir.conjure.spec.Documentation;
import java.util.List;
import java.util.Optional;
import org.immutables.value.Value;

@Value.Immutable
public interface ErrorSnippet extends PythonSnippet {
ImmutableList<PythonImport> DEFAULT_IMPORTS = ImmutableList.of(
PythonImport.builder()
.moduleSpecifier(ImportTypeVisitor.CONJURE_PYTHON_CLIENT)
.addNamedImports(NamedImport.of("ConjureHTTPError"))
.build(),
PythonImport.of("builtins"),
PythonImport.builder()
.moduleSpecifier(ImportTypeVisitor.TYPING)
.addNamedImports(NamedImport.of("TypedDict"))
.build());

@Override
@Value.Default
default String idForSorting() {
return className();
}

String className();

String definitionName();

PythonPackage definitionPackage();

Optional<Documentation> docs();

String errorCode();

String namespace();

List<PythonField> safeArgs();

List<PythonField> unsafeArgs();

@Override
default void emit(PythonPoetWriter poetWriter) {
poetWriter.writeIndentedLine(String.format("class %s(ConjureHTTPError):", className()));
poetWriter.increaseIndent();
docs().ifPresent(poetWriter::writeDocs);

poetWriter.writeLine();

// Error constants. ERROR_NAME is the fully-qualified wire form (e.g. "Datasets:DatasetNotFound") and
// matches the value of ConjureHTTPError.error_name parsed from the response body.
poetWriter.writeIndentedLine(String.format("ERROR_CODE = \"%s\"", errorCode()));
poetWriter.writeIndentedLine(String.format("ERROR_NAMESPACE = \"%s\"", namespace()));
poetWriter.writeIndentedLine(String.format("ERROR_NAME = \"%s:%s\"", namespace(), definitionName()));

poetWriter.writeLine();

// args
emitTypedDict(poetWriter, "SafeArgs", safeArgs());
emitTypedDict(poetWriter, "UnsafeArgs", unsafeArgs());

emitConstructor(poetWriter);

// classmethods
emitIsInstanceMethod(poetWriter);
emitFromErrorMethod(poetWriter);

// end of class def
poetWriter.decreaseIndent();
poetWriter.writeLine();
poetWriter.writeLine();

PythonClassRenamer.renameClass(poetWriter, className(), definitionPackage(), definitionName());
}

default void emitTypedDict(PythonPoetWriter poetWriter, String typedDictName, List<PythonField> fields) {
if (fields.isEmpty()) {
return;
}
poetWriter.writeIndentedLine(String.format("class %s(TypedDict):", typedDictName));
poetWriter.increaseIndent();
for (PythonField field : fields) {
poetWriter.writeIndentedLine(String.format(
"%s: %s", PythonIdentifierSanitizer.sanitize(field.attributeName()), field.myPyType()));
}
poetWriter.decreaseIndent();
poetWriter.writeLine();
}

default void emitConstructor(PythonPoetWriter poetWriter) {
poetWriter.writeIndentedLine("def __init__(self, base_error: ConjureHTTPError) -> None:");
poetWriter.increaseIndent();
poetWriter.writeIndentedLine("super().__init__(");
poetWriter.increaseIndent();
poetWriter.writeIndentedLine("status_code=base_error.status_code,");
// TODO(bzhang): Use enum once https://github.com/palantir/conjure-python-client/pull/171 is merged
poetWriter.writeIndentedLine("error_code=base_error.error_code,");
poetWriter.writeIndentedLine("error_name=base_error.error_name,");
poetWriter.writeIndentedLine("error_instance_id=base_error.error_instance_id,");
poetWriter.writeIndentedLine("parameters=base_error.parameters");
poetWriter.decreaseIndent();
poetWriter.writeIndentedLine(")");

emitArgsParser(poetWriter, "safe_args", "SafeArgs", safeArgs());
emitArgsParser(poetWriter, "unsafe_args", "UnsafeArgs", unsafeArgs());

poetWriter.decreaseIndent();
poetWriter.writeLine();
}

default void emitArgsParser(
PythonPoetWriter poetWriter, String fieldName, String typeName, List<PythonField> fields) {
if (fields.isEmpty()) {
return;
}
poetWriter.writeIndentedLine(String.format("self.%s: %s.%s = {", fieldName, className(), typeName));
poetWriter.increaseIndent();
for (int i = 0; i < fields.size(); i++) {
PythonField field = fields.get(i);
String comma = i == fields.size() - 1 ? "" : ",";
String lookup = field.isOptional()
? String.format("base_error.parameters.get('%s')", field.jsonIdentifier())
: String.format("base_error.parameters['%s']", field.jsonIdentifier());
poetWriter.writeIndentedLine(String.format(
"'%s': %s%s", PythonIdentifierSanitizer.sanitize(field.attributeName()), lookup, comma));
}
poetWriter.decreaseIndent();
poetWriter.writeIndentedLine("}");
}

default void emitIsInstanceMethod(PythonPoetWriter poetWriter) {
poetWriter.writeIndentedLine("@builtins.classmethod");
poetWriter.writeIndentedLine("def is_instance(cls, error: ConjureHTTPError) -> bool:");
poetWriter.increaseIndent();
poetWriter.writeIndentedLine("return (");
poetWriter.increaseIndent();
poetWriter.writeIndentedLine("error.error_name == cls.ERROR_NAME and");
poetWriter.writeIndentedLine("error.error_code == cls.ERROR_CODE");
poetWriter.decreaseIndent();
poetWriter.writeIndentedLine(")");
poetWriter.decreaseIndent();
poetWriter.writeLine();
}

default void emitFromErrorMethod(PythonPoetWriter poetWriter) {
poetWriter.writeIndentedLine("@builtins.classmethod");
poetWriter.writeIndentedLine(
String.format("def from_error(cls, error: ConjureHTTPError) -> '%s':", className()));
poetWriter.increaseIndent();
poetWriter.writeIndentedLine("if not cls.is_instance(error):");
poetWriter.increaseIndent();
poetWriter.writeIndentedLine("raise ValueError(f\"Error '{error.error_name}' is not a {cls.ERROR_NAME}\")");
poetWriter.decreaseIndent();
poetWriter.writeIndentedLine("return cls(error)");
poetWriter.decreaseIndent();
}

class Builder extends ImmutableErrorSnippet.Builder {}

static Builder builder() {
return new Builder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.conjure.python.types;

import com.palantir.conjure.CaseConverter;
import com.palantir.conjure.python.poet.ErrorSnippet;
import com.palantir.conjure.python.poet.PythonField;
import com.palantir.conjure.python.poet.PythonImport;
import com.palantir.conjure.python.poet.PythonPackage;
import com.palantir.conjure.python.processors.packagename.PackageNameProcessor;
import com.palantir.conjure.python.processors.typename.TypeNameProcessor;
import com.palantir.conjure.spec.ErrorDefinition;
import com.palantir.conjure.spec.FieldDefinition;
import com.palantir.conjure.visitor.DealiasingTypeVisitor;
import com.palantir.conjure.visitor.TypeVisitor;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public final class PythonErrorGenerator {

private final PackageNameProcessor implPackageNameProcessor;
private final TypeNameProcessor implTypeNameProcessor;
private final PackageNameProcessor definitionPackageNameProcessor;
private final TypeNameProcessor definitionTypeNameProcessor;
private final DealiasingTypeVisitor dealiasingTypeVisitor;
private final PythonTypeNameVisitor pythonTypeNameVisitor;
private final MyPyTypeNameVisitor myPyTypeNameVisitor;

public PythonErrorGenerator(
PackageNameProcessor implPackageNameProcessor,
TypeNameProcessor implTypeNameProcessor,
PackageNameProcessor definitionPackageNameProcessor,
TypeNameProcessor definitionTypeNameProcessor,
DealiasingTypeVisitor dealiasingTypeVisitor) {
this.implPackageNameProcessor = implPackageNameProcessor;
this.implTypeNameProcessor = implTypeNameProcessor;
this.definitionPackageNameProcessor = definitionPackageNameProcessor;
this.definitionTypeNameProcessor = definitionTypeNameProcessor;
this.dealiasingTypeVisitor = dealiasingTypeVisitor;
this.pythonTypeNameVisitor = new PythonTypeNameVisitor(implTypeNameProcessor);
this.myPyTypeNameVisitor = new MyPyTypeNameVisitor(dealiasingTypeVisitor, implTypeNameProcessor);
}

public ErrorSnippet generateError(ErrorDefinition errorDef) {
ImportTypeVisitor importVisitor =
new ImportTypeVisitor(errorDef.getErrorName(), implTypeNameProcessor, implPackageNameProcessor);

Set<PythonImport> imports = Stream.concat(errorDef.getSafeArgs().stream(), errorDef.getUnsafeArgs().stream())
.flatMap(entry -> entry.getType().accept(importVisitor).stream())
.collect(Collectors.toSet());

List<PythonField> safeArgs = toPythonFields(errorDef.getSafeArgs());
List<PythonField> unsafeArgs = toPythonFields(errorDef.getUnsafeArgs());

return ErrorSnippet.builder()
.pythonPackage(PythonPackage.of(
implPackageNameProcessor.process(errorDef.getErrorName().getPackage())))
.className(implTypeNameProcessor.process(errorDef.getErrorName()))
.definitionPackage(PythonPackage.of(definitionPackageNameProcessor.process(
errorDef.getErrorName().getPackage())))
.definitionName(definitionTypeNameProcessor.process(errorDef.getErrorName()))
.addAllImports(ErrorSnippet.DEFAULT_IMPORTS)
.addAllImports(imports)
.docs(errorDef.getDocs())
.errorCode(errorDef.getCode().toString())
.namespace(errorDef.getNamespace().toString())
.safeArgs(safeArgs)
.unsafeArgs(unsafeArgs)
.build();
}

private List<PythonField> toPythonFields(List<FieldDefinition> fields) {
return fields.stream()
.map(entry -> PythonField.builder()
.attributeName(CaseConverter.toCase(entry.getFieldName().get(), CaseConverter.Case.SNAKE_CASE))
.jsonIdentifier(entry.getFieldName().get())
.docs(entry.getDocs())
.pythonType(entry.getType().accept(pythonTypeNameVisitor))
.myPyType(entry.getType().accept(myPyTypeNameVisitor))
.isOptional(dealiasingTypeVisitor
.dealias(entry.getType())
.fold(_typeDefinition -> false, type -> type.accept(TypeVisitor.IS_OPTIONAL)))
.build())
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public final class ConjurePythonGeneratorTest {
.generatorVersion("0.0.0")
.shouldWriteCondaRecipe(true)
.generateRawSource(false)
.generateErrorTypes(true)
.build());
private final InMemoryPythonFileWriter pythonFileWriter = new InMemoryPythonFileWriter();

Expand Down
39 changes: 39 additions & 0 deletions conjure-python-core/src/test/resources/errors/example-errors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
types:
definitions:
default-package: com.palantir.product
objects:
Dataset:
fields:
fileSystemId: string
rid: string
errors:
DatasetNotFound:
namespace: Datasets
code: NOT_FOUND
safe-args:
datasetRid: string
availableDatasets: list<string>
docs: Thrown when the requested dataset does not exist

InvalidFileSystemId:
namespace: Datasets
code: INVALID_ARGUMENT
safe-args:
fileSystemId: string
reason: optional<string>
unsafe-args:
userId: string
docs: Thrown when a file system identifier is invalid

services:
DatasetService:
name: Dataset Service
package: com.palantir.product
base-path: /datasets
endpoints:
getDatasetsByFileSystem:
http: GET /fileSystem/{fileSystemId}
args:
fileSystemId: string
returns: list<Dataset>
docs: Get datasets by file system
Loading