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..4f6ec5f58 --- /dev/null +++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/poet/ErrorSnippet.java @@ -0,0 +1,179 @@ +/* + * (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 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(); + + // 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 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 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 ? "" : ","; + 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("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("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..33c179775 --- /dev/null +++ b/conjure-python-core/src/main/java/com/palantir/conjure/python/types/PythonErrorGenerator.java @@ -0,0 +1,114 @@ +/* + * (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.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..4e7cfa2e3 --- /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: + Dataset: + fields: + fileSystemId: string + rid: string + errors: + DatasetNotFound: + namespace: Datasets + code: NOT_FOUND + safe-args: + datasetRid: string + availableDatasets: list + docs: Thrown when the requested dataset does not exist + + InvalidFileSystemId: + namespace: Datasets + code: INVALID_ARGUMENT + safe-args: + fileSystemId: 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 + docs: Get datasets by file system 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..fbc7231b4 --- /dev/null +++ b/conjure-python-core/src/test/resources/errors/expected/package_name/_impl.py @@ -0,0 +1,183 @@ +# 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_Dataset(ConjureBeanType): + + @builtins.classmethod + def _fields(cls) -> Dict[str, ConjureFieldDefinition]: + return { + 'file_system_id': ConjureFieldDefinition('fileSystemId', str), + 'rid': ConjureFieldDefinition('rid', str) + } + + __slots__: List[str] = ['_file_system_id', '_rid'] + + def __init__(self, file_system_id: str, rid: str) -> None: + self._file_system_id = file_system_id + self._rid = rid + + @builtins.property + def file_system_id(self) -> str: + return self._file_system_id + + @builtins.property + def rid(self) -> str: + return self._rid + + +product_Dataset.__name__ = "Dataset" +product_Dataset.__qualname__ = "Dataset" +product_Dataset.__module__ = "package_name.product" + + +class product_DatasetNotFound(ConjureHTTPError): + """Thrown when the requested dataset does not exist + """ + + ERROR_CODE = "NOT_FOUND" + ERROR_NAMESPACE = "Datasets" + ERROR_NAME = "DatasetNotFound" + + class SafeArgs(TypedDict): + dataset_rid: str + available_datasets: 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_DatasetNotFound.SafeArgs = { + 'dataset_rid': base_error.parameters['datasetRid'], + 'available_datasets': base_error.parameters['availableDatasets'] + } + + @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_DatasetNotFound': + if not cls.is_instance(error): + raise ValueError(f"Error is not a {cls.ERROR_NAME}") + return cls(error) + + +product_DatasetNotFound.__name__ = "DatasetNotFound" +product_DatasetNotFound.__qualname__ = "DatasetNotFound" +product_DatasetNotFound.__module__ = "package_name.product" + + +class product_DatasetService(Service): + + def get_datasets_by_file_system(self, file_system_id: str) -> List["product_Dataset"]: + """Get datasets by file system + """ + _conjure_encoder = ConjureEncoder() + + _headers: Dict[str, Any] = { + 'Accept': 'application/json', + } + + _params: Dict[str, Any] = { + } + + _path_params: Dict[str, str] = { + 'fileSystemId': quote(str(_conjure_encoder.default(file_system_id)), safe=''), + } + + _json: Any = None + + _path = '/datasets/fileSystem/{fileSystemId}' + _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_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_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 new file mode 100644 index 000000000..631bb88c7 --- /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_Dataset as Dataset, + product_DatasetNotFound as DatasetNotFound, + product_DatasetService as DatasetService, + product_InvalidFileSystemId as InvalidFileSystemId, +) + +__all__ = [ + 'Dataset', + 'DatasetService', + 'DatasetNotFound', + 'InvalidFileSystemId', +] + 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(); } }