From 16f3b89dfb135a48f0abefeabb4293b16d512de3 Mon Sep 17 00:00:00 2001 From: Bowei Zhang Date: Mon, 20 Oct 2025 11:20:36 -0700 Subject: [PATCH 1/3] Feature: Add an option to generate exceptions for Conjure errors --- .../python/ConjurePythonGenerator.java | 25 +++ .../python/GeneratorConfiguration.java | 5 + .../conjure/python/poet/ErrorSnippet.java | 179 +++++++++++++++++ .../python/types/PythonErrorGenerator.java | 114 +++++++++++ .../python/ConjurePythonGeneratorTest.java | 1 + .../test/resources/errors/example-errors.yml | 38 ++++ .../errors/expected/conda_recipe/meta.yaml | 23 +++ .../errors/expected/package_name/__init__.py | 9 + .../errors/expected/package_name/_impl.py | 187 ++++++++++++++++++ .../expected/package_name/product/__init__.py | 15 ++ .../errors/expected/package_name/py.typed | 1 + .../test/resources/errors/expected/setup.py | 18 ++ .../conjure/python/cli/CliConfiguration.java | 6 + .../conjure/python/cli/ConjurePythonCli.java | 8 + 14 files changed, 629 insertions(+) create mode 100644 conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java create mode 100644 conjure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java create mode 100644 conjure-python-core/src/test/resources/errors/example-errors.yml create mode 100644 conjure-python-core/src/test/resources/errors/expected/conda_recipe/meta.yaml create mode 100644 conjure-python-core/src/test/resources/errors/expected/package_name/__init__.py create mode 100644 conjure-python-core/src/test/resources/errors/expected/package_name/_impl.py create mode 100644 conjure-python-core/src/test/resources/errors/expected/package_name/product/__init__.py create mode 100644 conjure-python-core/src/test/resources/errors/expected/package_name/py.typed create mode 100644 conjure-python-core/src/test/resources/errors/expected/setup.py diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/ConjurePythonGenerator.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/ConjurePythonGenerator.java index a6d386d92..2d10e49ef 100644 --- a/conjure-python-core/src/main/java/com/palantir/conjure/python/ConjurePythonGenerator.java +++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/ConjurePythonGenerator.java @@ -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; @@ -146,6 +147,12 @@ private PythonFile getImplPythonFile( definitionPackageNameProcessor, definitionTypeNameProcessor, dealiasingTypeVisitor); + PythonErrorGenerator errorGenerator = new PythonErrorGenerator( + implPackageNameProcessor, + implTypeNameProcessor, + definitionPackageNameProcessor, + definitionTypeNameProcessor, + dealiasingTypeVisitor); List snippets = new ArrayList<>(); snippets.addAll(conjureDefinition.getTypes().stream() @@ -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> snippetsByPackage = snippets.stream().collect(Collectors.groupingBy(PythonSnippet::pythonPackage)); @@ -203,6 +215,19 @@ private List 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 importNames = imports.stream() diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/GeneratorConfiguration.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/GeneratorConfiguration.java index a6ee31169..c99f2b580 100644 --- a/conjure-python-core/src/main/java/com/palantir/conjure/python/GeneratorConfiguration.java +++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/GeneratorConfiguration.java @@ -43,6 +43,11 @@ public interface GeneratorConfiguration { boolean generateRawSource(); + @Value.Default + default boolean generateErrorTypes() { + return false; + } + default Optional pythonicPackageName() { return packageName().map(packageName -> packageName.replace('-', '_')); } diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java new file mode 100644 index 000000000..a2659b41a --- /dev/null +++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java @@ -0,0 +1,179 @@ +/* + * (c) Copyright 2018 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 DEFAULT_IMPORTS = ImmutableList.of( + PythonImport.builder() + .moduleSpecifier(ImportTypeVisitor.CONJURE_PYTHON_CLIENT) + .addNamedImports(NamedImport.of("ConjureHTTPError")) + .build(), + 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 docs(); + + String errorCode(); + + String namespace(); + + List safeArgs(); + + List 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 + poetWriter.writeIndentedLine(String.format("ERROR_CODE = \"%s\"", errorCode())); + poetWriter.writeIndentedLine(String.format("ERROR_NAMESPACE = \"%s\"", namespace())); + poetWriter.writeIndentedLine(String.format("ERROR_NAME = \"%s\"", definitionName())); + + poetWriter.writeLine(); + + // TypedDicts for args + emitTypedDict(poetWriter, "SafeArgs", safeArgs()); + emitTypedDict(poetWriter, "UnsafeArgs", unsafeArgs()); + + // Constructor + 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 fields) { + if (!fields.isEmpty()) { + 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,"); + 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 fields) { + if (!fields.isEmpty()) { + 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 ? "" : ","; + poetWriter.writeIndentedLine(String.format( + "'%s': base_error.parameters['%s']%s", + PythonIdentifierSanitizer.sanitize(field.attributeName()), field.jsonIdentifier(), comma)); + } + poetWriter.decreaseIndent(); + poetWriter.writeIndentedLine("}"); + } + } + + default void emitIsInstanceMethod(PythonPoetWriter poetWriter) { + poetWriter.writeIndentedLine("@classmethod"); + poetWriter.writeIndentedLine("def is_instance(cls, error: ConjureHTTPError) -> bool:"); + poetWriter.increaseIndent(); + poetWriter.writeIndentedLine("\"\"\"Check if a ConjureHTTPError is this specific error type\"\"\""); + 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("@classmethod"); + poetWriter.writeIndentedLine( + String.format("def from_error(cls, error: ConjureHTTPError) -> '%s':", className())); + poetWriter.increaseIndent(); + poetWriter.writeIndentedLine("\"\"\"Convert a generic ConjureHTTPError to this typed error\"\"\""); + poetWriter.writeIndentedLine("if not cls.is_instance(error):"); + poetWriter.increaseIndent(); + poetWriter.writeIndentedLine("raise ValueError(f\"Error 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(); + } +} diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java new file mode 100644 index 000000000..992a3def6 --- /dev/null +++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java @@ -0,0 +1,114 @@ +/* + * (c) Copyright 2018 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.visitor.DealiasingTypeVisitor; +import com.palantir.conjure.visitor.TypeVisitor; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +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); + + // Collect imports from all field types + Set imports = errorDef.getSafeArgs().stream() + .flatMap(entry -> entry.getType().accept(importVisitor).stream()) + .collect(Collectors.toSet()); + imports.addAll(errorDef.getUnsafeArgs().stream() + .flatMap(entry -> entry.getType().accept(importVisitor).stream()) + .collect(Collectors.toSet())); + + // Convert safe args to PythonFields + List safeArgs = errorDef.getSafeArgs().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()); + + // Convert unsafe args to PythonFields + List unsafeArgs = errorDef.getUnsafeArgs().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()); + + 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(); + } +} diff --git a/conjure-python-core/src/test/java/com/palantir/conjure/python/ConjurePythonGeneratorTest.java b/conjure-python-core/src/test/java/com/palantir/conjure/python/ConjurePythonGeneratorTest.java index bd307bf23..f8e7321ac 100644 --- a/conjure-python-core/src/test/java/com/palantir/conjure/python/ConjurePythonGeneratorTest.java +++ b/conjure-python-core/src/test/java/com/palantir/conjure/python/ConjurePythonGeneratorTest.java @@ -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(); diff --git a/conjure-python-core/src/test/resources/errors/example-errors.yml b/conjure-python-core/src/test/resources/errors/example-errors.yml new file mode 100644 index 000000000..35d88f102 --- /dev/null +++ b/conjure-python-core/src/test/resources/errors/example-errors.yml @@ -0,0 +1,38 @@ +types: + definitions: + default-package: com.palantir.product + objects: + Recipe: + fields: + name: string + ingredients: list + errors: + CategoryNotFound: + namespace: Recipe + code: NOT_FOUND + safe-args: + categoryId: string + availableCategories: list + docs: Thrown when the requested recipe category doesn't exist + + InvalidIngredient: + namespace: Recipe + code: INVALID_ARGUMENT + safe-args: + ingredientName: string + unsafe-args: + userId: string + docs: Thrown when an ingredient is invalid for the recipe + +services: + RecipeService: + name: Recipe Service + package: com.palantir.product + base-path: /recipes + endpoints: + getRecipesByCategory: + http: GET /category/{categoryId} + args: + categoryId: string + returns: list + docs: Get recipes by category diff --git a/conjure-python-core/src/test/resources/errors/expected/conda_recipe/meta.yaml b/conjure-python-core/src/test/resources/errors/expected/conda_recipe/meta.yaml new file mode 100644 index 000000000..398009c87 --- /dev/null +++ b/conjure-python-core/src/test/resources/errors/expected/conda_recipe/meta.yaml @@ -0,0 +1,23 @@ +# coding=utf-8 +package: + name: package-name + version: 0.0.0 + +source: + path: ../ + +build: + noarch: python + script: python setup.py install --single-version-externally-managed --record=record.txt + +requirements: + build: + - python + - setuptools + - requests + - conjure-python-client >=2.8.0,<4 + + run: + - python + - requests + - conjure-python-client >=2.8.0,<4 diff --git a/conjure-python-core/src/test/resources/errors/expected/package_name/__init__.py b/conjure-python-core/src/test/resources/errors/expected/package_name/__init__.py new file mode 100644 index 000000000..ce0634d76 --- /dev/null +++ b/conjure-python-core/src/test/resources/errors/expected/package_name/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +__all__ = [ + 'product', +] + +__conjure_generator_version__ = "0.0.0" + +__version__ = "0.0.0" + diff --git a/conjure-python-core/src/test/resources/errors/expected/package_name/_impl.py b/conjure-python-core/src/test/resources/errors/expected/package_name/_impl.py new file mode 100644 index 000000000..fb11c74bb --- /dev/null +++ b/conjure-python-core/src/test/resources/errors/expected/package_name/_impl.py @@ -0,0 +1,187 @@ +# coding=utf-8 +import builtins +from conjure_python_client import ( + ConjureBeanType, + ConjureDecoder, + ConjureEncoder, + ConjureFieldDefinition, + ConjureHTTPError, + Service, +) +from requests.adapters import ( + Response, +) +from typing import ( + Any, + Dict, + List, + TypedDict, +) +from urllib.parse import ( + quote, +) + +class product_CategoryNotFound(ConjureHTTPError): + """Thrown when the requested recipe category doesn't exist + """ + + ERROR_CODE = "NOT_FOUND" + ERROR_NAMESPACE = "Recipe" + ERROR_NAME = "CategoryNotFound" + + class SafeArgs(TypedDict): + category_id: str + available_categories: List[str] + + def __init__(self, base_error: ConjureHTTPError) -> None: + super().__init__( + status_code=base_error.status_code, + error_code=base_error.error_code, + error_name=base_error.error_name, + error_instance_id=base_error.error_instance_id, + parameters=base_error.parameters + ) + self.safe_args: product_CategoryNotFound.SafeArgs = { + 'category_id': base_error.parameters['categoryId'], + 'available_categories': base_error.parameters['availableCategories'] + } + + @classmethod + def is_instance(cls, error: ConjureHTTPError) -> bool: + """Check if a ConjureHTTPError is this specific error type""" + return ( + error.error_name == cls.ERROR_NAME and + error.error_code == cls.ERROR_CODE + ) + + @classmethod + def from_error(cls, error: ConjureHTTPError) -> 'product_CategoryNotFound': + """Convert a generic ConjureHTTPError to this typed error""" + if not cls.is_instance(error): + raise ValueError(f"Error is not a {cls.ERROR_NAME}") + return cls(error) + + +product_CategoryNotFound.__name__ = "CategoryNotFound" +product_CategoryNotFound.__qualname__ = "CategoryNotFound" +product_CategoryNotFound.__module__ = "package_name.product" + + +class product_InvalidIngredient(ConjureHTTPError): + """Thrown when an ingredient is invalid for the recipe + """ + + ERROR_CODE = "INVALID_ARGUMENT" + ERROR_NAMESPACE = "Recipe" + ERROR_NAME = "InvalidIngredient" + + class SafeArgs(TypedDict): + ingredient_name: str + + class UnsafeArgs(TypedDict): + user_id: str + + def __init__(self, base_error: ConjureHTTPError) -> None: + super().__init__( + status_code=base_error.status_code, + error_code=base_error.error_code, + error_name=base_error.error_name, + error_instance_id=base_error.error_instance_id, + parameters=base_error.parameters + ) + self.safe_args: product_InvalidIngredient.SafeArgs = { + 'ingredient_name': base_error.parameters['ingredientName'] + } + self.unsafe_args: product_InvalidIngredient.UnsafeArgs = { + 'user_id': base_error.parameters['userId'] + } + + @classmethod + def is_instance(cls, error: ConjureHTTPError) -> bool: + """Check if a ConjureHTTPError is this specific error type""" + return ( + error.error_name == cls.ERROR_NAME and + error.error_code == cls.ERROR_CODE + ) + + @classmethod + def from_error(cls, error: ConjureHTTPError) -> 'product_InvalidIngredient': + """Convert a generic ConjureHTTPError to this typed error""" + if not cls.is_instance(error): + raise ValueError(f"Error is not a {cls.ERROR_NAME}") + return cls(error) + + +product_InvalidIngredient.__name__ = "InvalidIngredient" +product_InvalidIngredient.__qualname__ = "InvalidIngredient" +product_InvalidIngredient.__module__ = "package_name.product" + + +class product_Recipe(ConjureBeanType): + + @builtins.classmethod + def _fields(cls) -> Dict[str, ConjureFieldDefinition]: + return { + 'name': ConjureFieldDefinition('name', str), + 'ingredients': ConjureFieldDefinition('ingredients', List[str]) + } + + __slots__: List[str] = ['_name', '_ingredients'] + + def __init__(self, ingredients: List[str], name: str) -> None: + self._name = name + self._ingredients = ingredients + + @builtins.property + def name(self) -> str: + return self._name + + @builtins.property + def ingredients(self) -> List[str]: + return self._ingredients + + +product_Recipe.__name__ = "Recipe" +product_Recipe.__qualname__ = "Recipe" +product_Recipe.__module__ = "package_name.product" + + +class product_RecipeService(Service): + + def get_recipes_by_category(self, category_id: str) -> List["product_Recipe"]: + """Get recipes by category + """ + _conjure_encoder = ConjureEncoder() + + _headers: Dict[str, Any] = { + 'Accept': 'application/json', + } + + _params: Dict[str, Any] = { + } + + _path_params: Dict[str, str] = { + 'categoryId': quote(str(_conjure_encoder.default(category_id)), safe=''), + } + + _json: Any = None + + _path = '/recipes/category/{categoryId}' + _path = _path.format(**_path_params) + + _response: Response = self._request( + 'GET', + self._uri + _path, + params=_params, + headers=_headers, + json=_json) + + _decoder = ConjureDecoder() + return _decoder.decode(_response.json(), List[product_Recipe], self._return_none_for_unknown_union_types) + + +product_RecipeService.__name__ = "RecipeService" +product_RecipeService.__qualname__ = "RecipeService" +product_RecipeService.__module__ = "package_name.product" + + diff --git a/conjure-python-core/src/test/resources/errors/expected/package_name/product/__init__.py b/conjure-python-core/src/test/resources/errors/expected/package_name/product/__init__.py new file mode 100644 index 000000000..604b837aa --- /dev/null +++ b/conjure-python-core/src/test/resources/errors/expected/package_name/product/__init__.py @@ -0,0 +1,15 @@ +# coding=utf-8 +from .._impl import ( + product_CategoryNotFound as CategoryNotFound, + product_InvalidIngredient as InvalidIngredient, + product_Recipe as Recipe, + product_RecipeService as RecipeService, +) + +__all__ = [ + 'Recipe', + 'RecipeService', + 'CategoryNotFound', + 'InvalidIngredient', +] + diff --git a/conjure-python-core/src/test/resources/errors/expected/package_name/py.typed b/conjure-python-core/src/test/resources/errors/expected/package_name/py.typed new file mode 100644 index 000000000..9bad5790a --- /dev/null +++ b/conjure-python-core/src/test/resources/errors/expected/package_name/py.typed @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/conjure-python-core/src/test/resources/errors/expected/setup.py b/conjure-python-core/src/test/resources/errors/expected/setup.py new file mode 100644 index 000000000..42ad2060a --- /dev/null +++ b/conjure-python-core/src/test/resources/errors/expected/setup.py @@ -0,0 +1,18 @@ +# coding=utf-8 +from setuptools import ( + find_packages, + setup, +) + +setup( + name='package-name', + version='0.0.0', + python_requires='>=3.8', + description='project description', + package_data={"": ["py.typed"]}, + packages=find_packages(), + install_requires=[ + 'requests', + 'conjure-python-client>=2.8.0,<4', + ], +) diff --git a/conjure-python/src/main/java/com/palantir/conjure/python/cli/CliConfiguration.java b/conjure-python/src/main/java/com/palantir/conjure/python/cli/CliConfiguration.java index ad0294b27..1e32910e8 100644 --- a/conjure-python/src/main/java/com/palantir/conjure/python/cli/CliConfiguration.java +++ b/conjure-python/src/main/java/com/palantir/conjure/python/cli/CliConfiguration.java @@ -51,6 +51,12 @@ boolean shouldWriteCondaRecipe() { return false; } + @Value.Default + @SuppressWarnings("DesignForExtension") + boolean generateErrorTypes() { + return false; + } + @Value.Check final void check() { Preconditions.checkArgument(input().isFile(), "Target must exist and be a file"); diff --git a/conjure-python/src/main/java/com/palantir/conjure/python/cli/ConjurePythonCli.java b/conjure-python/src/main/java/com/palantir/conjure/python/cli/ConjurePythonCli.java index f46d25921..a24dc8e37 100644 --- a/conjure-python/src/main/java/com/palantir/conjure/python/cli/ConjurePythonCli.java +++ b/conjure-python/src/main/java/com/palantir/conjure/python/cli/ConjurePythonCli.java @@ -89,6 +89,12 @@ public static final class GenerateCommand implements Runnable { description = "Generate a `conda_recipe/meta.yaml`") private boolean writeCondaRecipe; + @CommandLine.Option( + names = "--generateErrorTypes", + defaultValue = "false", + description = "Generate typed error classes from Conjure error definitions") + private boolean generateErrorTypes; + @CommandLine.Unmatched @SuppressWarnings("StrictUnusedVariable") private List unmatchedOptions; @@ -118,6 +124,7 @@ CliConfiguration getConfiguration() { .packageUrl(Optional.ofNullable(packageUrl)) .generateRawSource(rawSource) .shouldWriteCondaRecipe(writeCondaRecipe) + .generateErrorTypes(generateErrorTypes) .build(); } @@ -134,6 +141,7 @@ static GeneratorConfiguration resolveGeneratorConfiguration( .packageUrl(cliConfig.packageUrl()) .shouldWriteCondaRecipe(cliConfig.shouldWriteCondaRecipe()) .generateRawSource(cliConfig.generateRawSource()) + .generateErrorTypes(cliConfig.generateErrorTypes()) .build(); } } From 27ecd725052e7c91d39c5687da0a8eb977120509 Mon Sep 17 00:00:00 2001 From: Bowei Zhang Date: Mon, 20 Oct 2025 14:56:57 -0700 Subject: [PATCH 2/3] clean up a bit --- .../conjure/python/poet/ErrorSnippet.java | 52 ++++++++++--------- .../python/types/PythonErrorGenerator.java | 2 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java index a2659b41a..6cffb8dfd 100644 --- a/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java +++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java @@ -1,5 +1,5 @@ /* - * (c) Copyright 2018 Palantir Technologies Inc. All rights reserved. + * (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. @@ -73,14 +73,13 @@ default void emit(PythonPoetWriter poetWriter) { poetWriter.writeLine(); - // TypedDicts for args + // args emitTypedDict(poetWriter, "SafeArgs", safeArgs()); emitTypedDict(poetWriter, "UnsafeArgs", unsafeArgs()); - // Constructor emitConstructor(poetWriter); - // Classmethods + // classmethods emitIsInstanceMethod(poetWriter); emitFromErrorMethod(poetWriter); @@ -93,16 +92,17 @@ default void emit(PythonPoetWriter poetWriter) { } default void emitTypedDict(PythonPoetWriter poetWriter, String typedDictName, List fields) { - if (!fields.isEmpty()) { - 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(); + 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) { @@ -111,6 +111,7 @@ default void emitConstructor(PythonPoetWriter poetWriter) { 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,"); @@ -127,19 +128,20 @@ default void emitConstructor(PythonPoetWriter poetWriter) { default void emitArgsParser( PythonPoetWriter poetWriter, String fieldName, String typeName, List fields) { - if (!fields.isEmpty()) { - 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 ? "" : ","; - poetWriter.writeIndentedLine(String.format( - "'%s': base_error.parameters['%s']%s", - PythonIdentifierSanitizer.sanitize(field.attributeName()), field.jsonIdentifier(), comma)); - } - poetWriter.decreaseIndent(); - poetWriter.writeIndentedLine("}"); + 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 ? "" : ","; + poetWriter.writeIndentedLine(String.format( + "'%s': base_error.parameters['%s']%s", + PythonIdentifierSanitizer.sanitize(field.attributeName()), field.jsonIdentifier(), comma)); + } + poetWriter.decreaseIndent(); + poetWriter.writeIndentedLine("}"); } default void emitIsInstanceMethod(PythonPoetWriter poetWriter) { diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java index 992a3def6..33c179775 100644 --- a/conjure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java +++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java @@ -1,5 +1,5 @@ /* - * (c) Copyright 2018 Palantir Technologies Inc. All rights reserved. + * (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. From 20d2a1f474022c2840e08f2acd94ed8d320b48f2 Mon Sep 17 00:00:00 2001 From: Bowei Zhang Date: Mon, 20 Oct 2025 16:28:02 -0700 Subject: [PATCH 3/3] example names --- .../conjure/python/poet/ErrorSnippet.java | 2 - .../test/resources/errors/example-errors.yml | 40 ++-- .../errors/expected/package_name/_impl.py | 186 +++++++++--------- .../expected/package_name/product/__init__.py | 16 +- 4 files changed, 119 insertions(+), 125 deletions(-) diff --git a/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java b/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java index 6cffb8dfd..4f6ec5f58 100644 --- a/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java +++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java @@ -148,7 +148,6 @@ default void emitIsInstanceMethod(PythonPoetWriter poetWriter) { poetWriter.writeIndentedLine("@classmethod"); poetWriter.writeIndentedLine("def is_instance(cls, error: ConjureHTTPError) -> bool:"); poetWriter.increaseIndent(); - poetWriter.writeIndentedLine("\"\"\"Check if a ConjureHTTPError is this specific error type\"\"\""); poetWriter.writeIndentedLine("return ("); poetWriter.increaseIndent(); poetWriter.writeIndentedLine("error.error_name == cls.ERROR_NAME and"); @@ -164,7 +163,6 @@ default void emitFromErrorMethod(PythonPoetWriter poetWriter) { poetWriter.writeIndentedLine( String.format("def from_error(cls, error: ConjureHTTPError) -> '%s':", className())); poetWriter.increaseIndent(); - poetWriter.writeIndentedLine("\"\"\"Convert a generic ConjureHTTPError to this typed error\"\"\""); poetWriter.writeIndentedLine("if not cls.is_instance(error):"); poetWriter.increaseIndent(); poetWriter.writeIndentedLine("raise ValueError(f\"Error is not a {cls.ERROR_NAME}\")"); diff --git a/conjure-python-core/src/test/resources/errors/example-errors.yml b/conjure-python-core/src/test/resources/errors/example-errors.yml index 35d88f102..4e7cfa2e3 100644 --- a/conjure-python-core/src/test/resources/errors/example-errors.yml +++ b/conjure-python-core/src/test/resources/errors/example-errors.yml @@ -2,37 +2,37 @@ types: definitions: default-package: com.palantir.product objects: - Recipe: + Dataset: fields: - name: string - ingredients: list + fileSystemId: string + rid: string errors: - CategoryNotFound: - namespace: Recipe + DatasetNotFound: + namespace: Datasets code: NOT_FOUND safe-args: - categoryId: string - availableCategories: list - docs: Thrown when the requested recipe category doesn't exist + datasetRid: string + availableDatasets: list + docs: Thrown when the requested dataset does not exist - InvalidIngredient: - namespace: Recipe + InvalidFileSystemId: + namespace: Datasets code: INVALID_ARGUMENT safe-args: - ingredientName: string + fileSystemId: string unsafe-args: userId: string - docs: Thrown when an ingredient is invalid for the recipe + docs: Thrown when a file system identifier is invalid services: - RecipeService: - name: Recipe Service + DatasetService: + name: Dataset Service package: com.palantir.product - base-path: /recipes + base-path: /datasets endpoints: - getRecipesByCategory: - http: GET /category/{categoryId} + getDatasetsByFileSystem: + http: GET /fileSystem/{fileSystemId} args: - categoryId: string - returns: list - docs: Get recipes by category + fileSystemId: string + returns: list + docs: Get datasets by file system diff --git a/conjure-python-core/src/test/resources/errors/expected/package_name/_impl.py b/conjure-python-core/src/test/resources/errors/expected/package_name/_impl.py index fb11c74bb..fbc7231b4 100644 --- a/conjure-python-core/src/test/resources/errors/expected/package_name/_impl.py +++ b/conjure-python-core/src/test/resources/errors/expected/package_name/_impl.py @@ -21,65 +21,46 @@ quote, ) -class product_CategoryNotFound(ConjureHTTPError): - """Thrown when the requested recipe category doesn't exist - """ +class product_Dataset(ConjureBeanType): - ERROR_CODE = "NOT_FOUND" - ERROR_NAMESPACE = "Recipe" - ERROR_NAME = "CategoryNotFound" + @builtins.classmethod + def _fields(cls) -> Dict[str, ConjureFieldDefinition]: + return { + 'file_system_id': ConjureFieldDefinition('fileSystemId', str), + 'rid': ConjureFieldDefinition('rid', str) + } - class SafeArgs(TypedDict): - category_id: str - available_categories: List[str] + __slots__: List[str] = ['_file_system_id', '_rid'] - def __init__(self, base_error: ConjureHTTPError) -> None: - super().__init__( - status_code=base_error.status_code, - error_code=base_error.error_code, - error_name=base_error.error_name, - error_instance_id=base_error.error_instance_id, - parameters=base_error.parameters - ) - self.safe_args: product_CategoryNotFound.SafeArgs = { - 'category_id': base_error.parameters['categoryId'], - 'available_categories': base_error.parameters['availableCategories'] - } + def __init__(self, file_system_id: str, rid: str) -> None: + self._file_system_id = file_system_id + self._rid = rid - @classmethod - def is_instance(cls, error: ConjureHTTPError) -> bool: - """Check if a ConjureHTTPError is this specific error type""" - return ( - error.error_name == cls.ERROR_NAME and - error.error_code == cls.ERROR_CODE - ) + @builtins.property + def file_system_id(self) -> str: + return self._file_system_id - @classmethod - def from_error(cls, error: ConjureHTTPError) -> 'product_CategoryNotFound': - """Convert a generic ConjureHTTPError to this typed error""" - if not cls.is_instance(error): - raise ValueError(f"Error is not a {cls.ERROR_NAME}") - return cls(error) + @builtins.property + def rid(self) -> str: + return self._rid -product_CategoryNotFound.__name__ = "CategoryNotFound" -product_CategoryNotFound.__qualname__ = "CategoryNotFound" -product_CategoryNotFound.__module__ = "package_name.product" +product_Dataset.__name__ = "Dataset" +product_Dataset.__qualname__ = "Dataset" +product_Dataset.__module__ = "package_name.product" -class product_InvalidIngredient(ConjureHTTPError): - """Thrown when an ingredient is invalid for the recipe +class product_DatasetNotFound(ConjureHTTPError): + """Thrown when the requested dataset does not exist """ - ERROR_CODE = "INVALID_ARGUMENT" - ERROR_NAMESPACE = "Recipe" - ERROR_NAME = "InvalidIngredient" + ERROR_CODE = "NOT_FOUND" + ERROR_NAMESPACE = "Datasets" + ERROR_NAME = "DatasetNotFound" class SafeArgs(TypedDict): - ingredient_name: str - - class UnsafeArgs(TypedDict): - user_id: str + dataset_rid: str + available_datasets: List[str] def __init__(self, base_error: ConjureHTTPError) -> None: super().__init__( @@ -89,67 +70,34 @@ def __init__(self, base_error: ConjureHTTPError) -> None: error_instance_id=base_error.error_instance_id, parameters=base_error.parameters ) - self.safe_args: product_InvalidIngredient.SafeArgs = { - 'ingredient_name': base_error.parameters['ingredientName'] - } - self.unsafe_args: product_InvalidIngredient.UnsafeArgs = { - 'user_id': base_error.parameters['userId'] + self.safe_args: product_DatasetNotFound.SafeArgs = { + 'dataset_rid': base_error.parameters['datasetRid'], + 'available_datasets': base_error.parameters['availableDatasets'] } @classmethod def is_instance(cls, error: ConjureHTTPError) -> bool: - """Check if a ConjureHTTPError is this specific error type""" return ( error.error_name == cls.ERROR_NAME and error.error_code == cls.ERROR_CODE ) @classmethod - def from_error(cls, error: ConjureHTTPError) -> 'product_InvalidIngredient': - """Convert a generic ConjureHTTPError to this typed error""" + def from_error(cls, error: ConjureHTTPError) -> 'product_DatasetNotFound': if not cls.is_instance(error): raise ValueError(f"Error is not a {cls.ERROR_NAME}") return cls(error) -product_InvalidIngredient.__name__ = "InvalidIngredient" -product_InvalidIngredient.__qualname__ = "InvalidIngredient" -product_InvalidIngredient.__module__ = "package_name.product" - - -class product_Recipe(ConjureBeanType): - - @builtins.classmethod - def _fields(cls) -> Dict[str, ConjureFieldDefinition]: - return { - 'name': ConjureFieldDefinition('name', str), - 'ingredients': ConjureFieldDefinition('ingredients', List[str]) - } - - __slots__: List[str] = ['_name', '_ingredients'] - - def __init__(self, ingredients: List[str], name: str) -> None: - self._name = name - self._ingredients = ingredients - - @builtins.property - def name(self) -> str: - return self._name - - @builtins.property - def ingredients(self) -> List[str]: - return self._ingredients - +product_DatasetNotFound.__name__ = "DatasetNotFound" +product_DatasetNotFound.__qualname__ = "DatasetNotFound" +product_DatasetNotFound.__module__ = "package_name.product" -product_Recipe.__name__ = "Recipe" -product_Recipe.__qualname__ = "Recipe" -product_Recipe.__module__ = "package_name.product" +class product_DatasetService(Service): -class product_RecipeService(Service): - - def get_recipes_by_category(self, category_id: str) -> List["product_Recipe"]: - """Get recipes by category + def get_datasets_by_file_system(self, file_system_id: str) -> List["product_Dataset"]: + """Get datasets by file system """ _conjure_encoder = ConjureEncoder() @@ -161,12 +109,12 @@ def get_recipes_by_category(self, category_id: str) -> List["product_Recipe"]: } _path_params: Dict[str, str] = { - 'categoryId': quote(str(_conjure_encoder.default(category_id)), safe=''), + 'fileSystemId': quote(str(_conjure_encoder.default(file_system_id)), safe=''), } _json: Any = None - _path = '/recipes/category/{categoryId}' + _path = '/datasets/fileSystem/{fileSystemId}' _path = _path.format(**_path_params) _response: Response = self._request( @@ -177,11 +125,59 @@ def get_recipes_by_category(self, category_id: str) -> List["product_Recipe"]: json=_json) _decoder = ConjureDecoder() - return _decoder.decode(_response.json(), List[product_Recipe], self._return_none_for_unknown_union_types) + return _decoder.decode(_response.json(), List[product_Dataset], self._return_none_for_unknown_union_types) + + +product_DatasetService.__name__ = "DatasetService" +product_DatasetService.__qualname__ = "DatasetService" +product_DatasetService.__module__ = "package_name.product" + + +class product_InvalidFileSystemId(ConjureHTTPError): + """Thrown when a file system identifier is invalid + """ + + ERROR_CODE = "INVALID_ARGUMENT" + ERROR_NAMESPACE = "Datasets" + ERROR_NAME = "InvalidFileSystemId" + + class SafeArgs(TypedDict): + file_system_id: str + + class UnsafeArgs(TypedDict): + user_id: str + + def __init__(self, base_error: ConjureHTTPError) -> None: + super().__init__( + status_code=base_error.status_code, + error_code=base_error.error_code, + error_name=base_error.error_name, + error_instance_id=base_error.error_instance_id, + parameters=base_error.parameters + ) + self.safe_args: product_InvalidFileSystemId.SafeArgs = { + 'file_system_id': base_error.parameters['fileSystemId'] + } + self.unsafe_args: product_InvalidFileSystemId.UnsafeArgs = { + 'user_id': base_error.parameters['userId'] + } + + @classmethod + def is_instance(cls, error: ConjureHTTPError) -> bool: + return ( + error.error_name == cls.ERROR_NAME and + error.error_code == cls.ERROR_CODE + ) + + @classmethod + def from_error(cls, error: ConjureHTTPError) -> 'product_InvalidFileSystemId': + if not cls.is_instance(error): + raise ValueError(f"Error is not a {cls.ERROR_NAME}") + return cls(error) -product_RecipeService.__name__ = "RecipeService" -product_RecipeService.__qualname__ = "RecipeService" -product_RecipeService.__module__ = "package_name.product" +product_InvalidFileSystemId.__name__ = "InvalidFileSystemId" +product_InvalidFileSystemId.__qualname__ = "InvalidFileSystemId" +product_InvalidFileSystemId.__module__ = "package_name.product" diff --git a/conjure-python-core/src/test/resources/errors/expected/package_name/product/__init__.py b/conjure-python-core/src/test/resources/errors/expected/package_name/product/__init__.py index 604b837aa..631bb88c7 100644 --- a/conjure-python-core/src/test/resources/errors/expected/package_name/product/__init__.py +++ b/conjure-python-core/src/test/resources/errors/expected/package_name/product/__init__.py @@ -1,15 +1,15 @@ # coding=utf-8 from .._impl import ( - product_CategoryNotFound as CategoryNotFound, - product_InvalidIngredient as InvalidIngredient, - product_Recipe as Recipe, - product_RecipeService as RecipeService, + product_Dataset as Dataset, + product_DatasetNotFound as DatasetNotFound, + product_DatasetService as DatasetService, + product_InvalidFileSystemId as InvalidFileSystemId, ) __all__ = [ - 'Recipe', - 'RecipeService', - 'CategoryNotFound', - 'InvalidIngredient', + 'Dataset', + 'DatasetService', + 'DatasetNotFound', + 'InvalidFileSystemId', ]