From 0d1df6411cd6f85a62f5010db88e6c22660647e3 Mon Sep 17 00:00:00 2001 From: Paulo Costa Date: Wed, 19 Oct 2022 21:44:21 -0300 Subject: [PATCH 1/5] Support multiple tags in each endpoint --- openapi_python_client/parser/openapi.py | 34 ++++++++++------ tests/test_parser/test_openapi.py | 52 +++++++++++++------------ 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index b6c2a5411..1328199da 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -57,13 +57,22 @@ def from_data( operation: Optional[oai.Operation] = getattr(path_data, method) if operation is None: continue - tag = utils.PythonIdentifier(value=(operation.tags or ["default"])[0], prefix="tag") - collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag)) + + tags = [ + utils.PythonIdentifier(value=tag, prefix="tag") + for tag in operation.tags or ["default"] + ] + + collections = [ + endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag)) + for tag in tags + ] + endpoint, schemas, parameters = Endpoint.from_data( data=operation, path=path, method=method, - tag=tag, + tags=tags, schemas=schemas, parameters=parameters, config=config, @@ -77,14 +86,17 @@ def from_data( endpoint = Endpoint.sort_parameters(endpoint=endpoint) if isinstance(endpoint, ParseError): endpoint.header = ( - f"WARNING parsing {method.upper()} {path} within {tag}. Endpoint will not be generated." + f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}. Endpoint will not be generated." ) - collection.parse_errors.append(endpoint) + for collection in collections: + collection.parse_errors.append(endpoint) continue for error in endpoint.errors: - error.header = f"WARNING parsing {method.upper()} {path} within {tag}." - collection.parse_errors.append(error) - collection.endpoints.append(endpoint) + error.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}." + for collection in collections: + collection.parse_errors.append(error) + for collection in collections: + collection.endpoints.append(endpoint) return endpoints_by_tag, schemas, parameters @@ -111,7 +123,7 @@ class Endpoint: description: Optional[str] name: str requires_security: bool - tag: str + tags: List[str] summary: Optional[str] = "" relative_imports: Set[str] = field(default_factory=set) query_parameters: Dict[str, Property] = field(default_factory=dict) @@ -464,7 +476,7 @@ def from_data( data: oai.Operation, path: str, method: str, - tag: str, + tags: List[str], schemas: Schemas, parameters: Parameters, config: Config, @@ -483,7 +495,7 @@ def from_data( description=utils.remove_string_escapes(data.description) if data.description else "", name=name, requires_security=bool(data.security), - tag=tag, + tags=tags, ) result, schemas, parameters = Endpoint.add_parameters( diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index a844e4172..a398436dc 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -130,7 +130,7 @@ def make_endpoint(self): description=None, name="name", requires_security=False, - tag="tag", + tags=["tag"], relative_imports={"import_3"}, ) @@ -981,7 +981,7 @@ def test_from_data_bad_params(self, mocker): data=data, path=path, method=method, - tag="default", + tags=["default"], schemas=initial_schemas, parameters=parameters, config=config, @@ -1016,7 +1016,7 @@ def test_from_data_bad_responses(self, mocker): data=data, path=path, method=method, - tag="default", + tags=["default"], schemas=initial_schemas, parameters=initial_parameters, config=config, @@ -1059,7 +1059,7 @@ def test_from_data_standard(self, mocker): data=data, path=path, method=method, - tag="default", + tags=["default"], schemas=initial_schemas, parameters=initial_parameters, config=config, @@ -1075,7 +1075,7 @@ def test_from_data_standard(self, mocker): summary="", name=data.operationId, requires_security=True, - tag="default", + tags=["default"], ), data=data, schemas=initial_schemas, @@ -1113,7 +1113,7 @@ def test_from_data_no_operation_id(self, mocker): parameters = mocker.MagicMock() endpoint, return_schemas, return_params = Endpoint.from_data( - data=data, path=path, method=method, tag="default", schemas=schemas, parameters=parameters, config=config + data=data, path=path, method=method, tags=["default"], schemas=schemas, parameters=parameters, config=config ) assert (endpoint, return_schemas) == _add_body.return_value @@ -1126,7 +1126,7 @@ def test_from_data_no_operation_id(self, mocker): summary="", name="get_path_with_param", requires_security=True, - tag="default", + tags=["default"], ), data=data, schemas=schemas, @@ -1167,7 +1167,7 @@ def test_from_data_no_security(self, mocker): config = MagicMock() Endpoint.from_data( - data=data, path=path, method=method, tag="a", schemas=schemas, parameters=parameters, config=config + data=data, path=path, method=method, tags=["a"], schemas=schemas, parameters=parameters, config=config ) add_parameters.assert_called_once_with( @@ -1178,7 +1178,7 @@ def test_from_data_no_security(self, mocker): summary="", name=data.operationId, requires_security=False, - tag="a", + tags=["a"], ), data=data, parameters=parameters, @@ -1241,9 +1241,9 @@ def test_from_data(self, mocker): "path_1": oai.PathItem.construct(post=path_1_post, put=path_1_put), "path_2": oai.PathItem.construct(get=path_2_get), } - endpoint_1 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"1", "2"}, path="path_1") - endpoint_2 = mocker.MagicMock(autospec=Endpoint, tag="tag_2", relative_imports={"2"}, path="path_1") - endpoint_3 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"2", "3"}, path="path_2") + endpoint_1 = mocker.MagicMock(autospec=Endpoint, tags=["default"], relative_imports={"1", "2"}, path="path_1") + endpoint_2 = mocker.MagicMock(autospec=Endpoint, tags=["tag_2"], relative_imports={"2"}, path="path_1") + endpoint_3 = mocker.MagicMock(autospec=Endpoint, tags=["default"], relative_imports={"2", "3"}, path="path_2") schemas_1 = mocker.MagicMock() schemas_2 = mocker.MagicMock() schemas_3 = mocker.MagicMock() @@ -1271,7 +1271,7 @@ def test_from_data(self, mocker): data=path_1_put, path="path_1", method="put", - tag="default", + tags=["default"], schemas=schemas, parameters=parameters, config=config, @@ -1280,7 +1280,7 @@ def test_from_data(self, mocker): data=path_1_post, path="path_1", method="post", - tag="tag_2", + tags=["tag_2", "tag_3"], schemas=schemas_1, parameters=parameters_1, config=config, @@ -1289,7 +1289,7 @@ def test_from_data(self, mocker): data=path_2_get, path="path_2", method="get", - tag="default", + tags=["default"], schemas=schemas_2, parameters=parameters_2, config=config, @@ -1300,6 +1300,7 @@ def test_from_data(self, mocker): { "default": EndpointCollection("default", endpoints=[endpoint_1, endpoint_3]), "tag_2": EndpointCollection("tag_2", endpoints=[endpoint_2]), + "tag_3": EndpointCollection("tag_3", endpoints=[endpoint_2]), }, schemas_3, parameters_3, @@ -1372,7 +1373,7 @@ def test_from_data_errors(self, mocker): data=path_1_put, path="path_1", method="put", - tag="default", + tags=["default"], schemas=schemas, parameters=parameters, config=config, @@ -1381,7 +1382,7 @@ def test_from_data_errors(self, mocker): data=path_1_post, path="path_1", method="post", - tag="tag_2", + tags=["tag_2", "tag_3"], schemas=schemas_1, parameters=parameters_1, config=config, @@ -1390,7 +1391,7 @@ def test_from_data_errors(self, mocker): data=path_2_get, path="path_2", method="get", - tag="default", + tags=["default"], schemas=schemas_2, parameters=parameters_2, config=config, @@ -1412,11 +1413,11 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): "path_1": oai.PathItem.construct(post=path_1_post, put=path_1_put), "path_2": oai.PathItem.construct(get=path_2_get), } - endpoint_1 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"1", "2"}, path="path_1") + endpoint_1 = mocker.MagicMock(autospec=Endpoint, tags=["default"], relative_imports={"1", "2"}, path="path_1") endpoint_2 = mocker.MagicMock( - autospec=Endpoint, tag="AMFSubscriptionInfo (Document)", relative_imports={"2"}, path="path_1" + autospec=Endpoint, tags=["AMFSubscriptionInfo (Document)"], relative_imports={"2"}, path="path_1" ) - endpoint_3 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"2", "3"}, path="path_2") + endpoint_3 = mocker.MagicMock(autospec=Endpoint, tags=["default"], relative_imports={"2", "3"}, path="path_2") schemas_1 = mocker.MagicMock() schemas_2 = mocker.MagicMock() schemas_3 = mocker.MagicMock() @@ -1444,7 +1445,7 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): data=path_1_put, path="path_1", method="put", - tag="default", + tags=["default"], schemas=schemas, parameters=parameters, config=config, @@ -1453,7 +1454,7 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): data=path_1_post, path="path_1", method="post", - tag="amf_subscription_info_document", + tags=["amf_subscription_info_document", "tag_3"], schemas=schemas_1, parameters=parameters_1, config=config, @@ -1462,7 +1463,7 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): data=path_2_get, path="path_2", method="get", - tag="tag3_abc", + tags=["tag3_abc"], schemas=schemas_2, parameters=parameters_2, config=config, @@ -1475,6 +1476,9 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): "amf_subscription_info_document": EndpointCollection( "amf_subscription_info_document", endpoints=[endpoint_2] ), + "tag_3": EndpointCollection( + "tag_3", endpoints=[endpoint_2] + ), "tag3_abc": EndpointCollection("tag3_abc", endpoints=[endpoint_3]), }, schemas_3, From cad0989b7584acf2886a6c57d54613174e8187d6 Mon Sep 17 00:00:00 2001 From: Paulo Costa Date: Mon, 14 Aug 2023 14:06:00 -0300 Subject: [PATCH 2/5] Fix formatting --- openapi_python_client/parser/openapi.py | 14 +++----------- tests/test_parser/test_openapi.py | 4 +--- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index db8ffab77..af56f479b 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -59,15 +59,9 @@ def from_data( if operation is None: continue - tags = [ - utils.PythonIdentifier(value=tag, prefix="tag") - for tag in operation.tags or ["default"] - ] + tags = [utils.PythonIdentifier(value=tag, prefix="tag") for tag in operation.tags or ["default"]] - collections = [ - endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag)) - for tag in tags - ] + collections = [endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag)) for tag in tags] endpoint, schemas, parameters = Endpoint.from_data( data=operation, @@ -86,9 +80,7 @@ def from_data( if not isinstance(endpoint, ParseError): endpoint = Endpoint.sort_parameters(endpoint=endpoint) if isinstance(endpoint, ParseError): - endpoint.header = ( - f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}. Endpoint will not be generated." - ) + endpoint.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}. Endpoint will not be generated." for collection in collections: collection.parse_errors.append(endpoint) continue diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index bb8fb2c6c..a3b23737a 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -1484,9 +1484,7 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): "amf_subscription_info_document": EndpointCollection( "amf_subscription_info_document", endpoints=[endpoint_2] ), - "tag_3": EndpointCollection( - "tag_3", endpoints=[endpoint_2] - ), + "tag_3": EndpointCollection("tag_3", endpoints=[endpoint_2]), "tag3_abc": EndpointCollection("tag3_abc", endpoints=[endpoint_3]), }, schemas_3, From 069842efba3fc9a86c25ac832023d514be5acede Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Tue, 24 Dec 2024 16:18:36 -0700 Subject: [PATCH 3/5] Document new config option --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 871f3a296..a184be377 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,16 @@ literal_enums: true This is especially useful if enum values, when transformed to their Python names, end up conflicting due to case sensitivity or special symbols. +### generate_all_tags + +`openapi-python-client` generates module names within the `api` module based on the OpenAPI `tags` of each endpoint. +By default, only the _first_ tag is generated. If you want to generate **duplicate** endpoint functions using _every_ tag +listed, you can enable this option: + +```yaml +generate_all_tags: true +``` + ### project_name_override and package_name_override Used to change the name of generated client library project/package. If the project name is changed but an override for the package name From a3ffbd8d6d05cdceaac2fc82f5fe0476783d8932 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Tue, 24 Dec 2024 16:20:02 -0700 Subject: [PATCH 4/5] Changeset --- .changeset/add_generate_all_tags_config_option.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/add_generate_all_tags_config_option.md diff --git a/.changeset/add_generate_all_tags_config_option.md b/.changeset/add_generate_all_tags_config_option.md new file mode 100644 index 000000000..fb74b9fb0 --- /dev/null +++ b/.changeset/add_generate_all_tags_config_option.md @@ -0,0 +1,8 @@ +--- +default: minor +--- + +# Add `generate_all_tags` config option + +You can now, optionally, generate **duplicate** endpoint functions/modules using _every_ tag for an endpoint, +not just the first one, by setting `generate_all_tags: true` in your configuration file. From 7f5cdd78ee2421db0deec4af126a8dec65af052e Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Tue, 24 Dec 2024 16:28:31 -0700 Subject: [PATCH 5/5] Add snapshot test to replace mock coverage --- .../__snapshots__/test_end_to_end.ambr | 14 ++++++++++++++ .../documents_with_errors/bad-status-code.yaml | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 end_to_end_tests/documents_with_errors/bad-status-code.yaml diff --git a/end_to_end_tests/__snapshots__/test_end_to_end.ambr b/end_to_end_tests/__snapshots__/test_end_to_end.ambr index c87445ffb..525f8baf2 100644 --- a/end_to_end_tests/__snapshots__/test_end_to_end.ambr +++ b/end_to_end_tests/__snapshots__/test_end_to_end.ambr @@ -1,4 +1,18 @@ # serializer version: 1 +# name: test_documents_with_errors[bad-status-code] + ''' + Generating /test-documents-with-errors + Warning(s) encountered while generating. Client was generated, but some pieces may be missing + + WARNING parsing GET / within default. + + Invalid response status code abcdef (not a valid HTTP status code), response will be omitted from generated client + + + If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose + + ''' +# --- # name: test_documents_with_errors[circular-body-ref] ''' Generating /test-documents-with-errors diff --git a/end_to_end_tests/documents_with_errors/bad-status-code.yaml b/end_to_end_tests/documents_with_errors/bad-status-code.yaml new file mode 100644 index 000000000..17c3ab2cf --- /dev/null +++ b/end_to_end_tests/documents_with_errors/bad-status-code.yaml @@ -0,0 +1,14 @@ +openapi: "3.1.0" +info: + title: "There's something wrong with me" + version: "0.1.0" +paths: + "/": + get: + responses: + "abcdef": + description: "Successful Response" + content: + "application/json": + schema: + const: "Why have a fixed response? I dunno"