Skip to content

Commit 88b3be1

Browse files
paulo-racadbanty
andauthored
Support multiple tags in each endpoint (openapi-generators#687)
Currently when an endpoint has multiple tags, the first tag is used and everything else is ignored. This PR modifies it so endpoints with multiple tags are added to each of the tags. Yes, this results in repeated code 😅, but works beautifully and functions can now be found anywhere we expect them to be. --------- Co-authored-by: Dylan Anthony <[email protected]> Co-authored-by: Dylan Anthony <[email protected]>
1 parent 7225f0e commit 88b3be1

File tree

14 files changed

+175
-120
lines changed

14 files changed

+175
-120
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
default: minor
3+
---
4+
5+
# Add `generate_all_tags` config option
6+
7+
You can now, optionally, generate **duplicate** endpoint functions/modules using _every_ tag for an endpoint,
8+
not just the first one, by setting `generate_all_tags: true` in your configuration file.

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ literal_enums: true
108108

109109
This is especially useful if enum values, when transformed to their Python names, end up conflicting due to case sensitivity or special symbols.
110110

111+
### generate_all_tags
112+
113+
`openapi-python-client` generates module names within the `api` module based on the OpenAPI `tags` of each endpoint.
114+
By default, only the _first_ tag is generated. If you want to generate **duplicate** endpoint functions using _every_ tag
115+
listed, you can enable this option:
116+
117+
```yaml
118+
generate_all_tags: true
119+
```
120+
111121
### project_name_override and package_name_override
112122

113123
Used to change the name of generated client library project/package. If the project name is changed but an override for the package name

end_to_end_tests/__snapshots__/test_end_to_end.ambr

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
# serializer version: 1
2+
# name: test_documents_with_errors[bad-status-code]
3+
'''
4+
Generating /test-documents-with-errors
5+
Warning(s) encountered while generating. Client was generated, but some pieces may be missing
6+
7+
WARNING parsing GET / within default.
8+
9+
Invalid response status code abcdef (not a valid HTTP status code), response will be omitted from generated client
10+
11+
12+
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
13+
14+
'''
15+
# ---
216
# name: test_documents_with_errors[circular-body-ref]
317
'''
418
Generating /test-documents-with-errors

end_to_end_tests/baseline_openapi_3.0.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -1149,9 +1149,7 @@
11491149
},
11501150
"/tag_with_number": {
11511151
"get": {
1152-
"tags": [
1153-
"1"
1154-
],
1152+
"tags": ["1", "2"],
11551153
"responses": {
11561154
"200": {
11571155
"description": "Success"

end_to_end_tests/baseline_openapi_3.1.yaml

+1-3
Original file line numberDiff line numberDiff line change
@@ -1141,9 +1141,7 @@ info:
11411141
},
11421142
"/tag_with_number": {
11431143
"get": {
1144-
"tags": [
1145-
"1"
1146-
],
1144+
"tags": ["1", "2"],
11471145
"responses": {
11481146
"200": {
11491147
"description": "Success"

end_to_end_tests/config.yml

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ class_overrides:
1111
field_prefix: attr_
1212
content_type_overrides:
1313
openapi/python/client: application/json
14+
generate_all_tags: true

end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .parameters import ParametersEndpoints
1212
from .responses import ResponsesEndpoints
1313
from .tag1 import Tag1Endpoints
14+
from .tag2 import Tag2Endpoints
1415
from .tests import TestsEndpoints
1516
from .true_ import True_Endpoints
1617

@@ -48,6 +49,10 @@ def parameters(cls) -> type[ParametersEndpoints]:
4849
def tag1(cls) -> type[Tag1Endpoints]:
4950
return Tag1Endpoints
5051

52+
@classmethod
53+
def tag2(cls) -> type[Tag2Endpoints]:
54+
return Tag2Endpoints
55+
5156
@classmethod
5257
def location(cls) -> type[LocationEndpoints]:
5358
return LocationEndpoints
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Contains methods for accessing the API Endpoints"""
2+
3+
import types
4+
5+
from . import get_tag_with_number
6+
7+
8+
class Tag2Endpoints:
9+
@classmethod
10+
def get_tag_with_number(cls) -> types.ModuleType:
11+
return get_tag_with_number
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
openapi: "3.1.0"
2+
info:
3+
title: "There's something wrong with me"
4+
version: "0.1.0"
5+
paths:
6+
"/":
7+
get:
8+
responses:
9+
"abcdef":
10+
description: "Successful Response"
11+
content:
12+
"application/json":
13+
schema:
14+
const: "Why have a fixed response? I dunno"

end_to_end_tests/golden-record/my_test_api_client/api/tag2/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from http import HTTPStatus
2+
from typing import Any, Optional, Union
3+
4+
import httpx
5+
6+
from ... import errors
7+
from ...client import AuthenticatedClient, Client
8+
from ...types import Response
9+
10+
11+
def _get_kwargs() -> dict[str, Any]:
12+
_kwargs: dict[str, Any] = {
13+
"method": "get",
14+
"url": "/tag_with_number",
15+
}
16+
17+
return _kwargs
18+
19+
20+
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
21+
if response.status_code == 200:
22+
return None
23+
if client.raise_on_unexpected_status:
24+
raise errors.UnexpectedStatus(response.status_code, response.content)
25+
else:
26+
return None
27+
28+
29+
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
30+
return Response(
31+
status_code=HTTPStatus(response.status_code),
32+
content=response.content,
33+
headers=response.headers,
34+
parsed=_parse_response(client=client, response=response),
35+
)
36+
37+
38+
def sync_detailed(
39+
*,
40+
client: Union[AuthenticatedClient, Client],
41+
) -> Response[Any]:
42+
"""
43+
Raises:
44+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
45+
httpx.TimeoutException: If the request takes longer than Client.timeout.
46+
47+
Returns:
48+
Response[Any]
49+
"""
50+
51+
kwargs = _get_kwargs()
52+
53+
response = client.get_httpx_client().request(
54+
**kwargs,
55+
)
56+
57+
return _build_response(client=client, response=response)
58+
59+
60+
async def asyncio_detailed(
61+
*,
62+
client: Union[AuthenticatedClient, Client],
63+
) -> Response[Any]:
64+
"""
65+
Raises:
66+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
67+
httpx.TimeoutException: If the request takes longer than Client.timeout.
68+
69+
Returns:
70+
Response[Any]
71+
"""
72+
73+
kwargs = _get_kwargs()
74+
75+
response = await client.get_async_httpx_client().request(**kwargs)
76+
77+
return _build_response(client=client, response=response)

openapi_python_client/config.py

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ConfigFile(BaseModel):
4242
use_path_prefixes_for_title_model_names: bool = True
4343
post_hooks: Optional[list[str]] = None
4444
field_prefix: str = "field_"
45+
generate_all_tags: bool = False
4546
http_timeout: int = 5
4647
literal_enums: bool = False
4748

@@ -70,6 +71,7 @@ class Config:
7071
use_path_prefixes_for_title_model_names: bool
7172
post_hooks: list[str]
7273
field_prefix: str
74+
generate_all_tags: bool
7375
http_timeout: int
7476
literal_enums: bool
7577
document_source: Union[Path, str]
@@ -110,6 +112,7 @@ def from_sources(
110112
use_path_prefixes_for_title_model_names=config_file.use_path_prefixes_for_title_model_names,
111113
post_hooks=post_hooks,
112114
field_prefix=config_file.field_prefix,
115+
generate_all_tags=config_file.generate_all_tags,
113116
http_timeout=config_file.http_timeout,
114117
literal_enums=config_file.literal_enums,
115118
document_source=document_source,

openapi_python_client/parser/openapi.py

+19-13
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,18 @@ def from_data(
6363
operation: Optional[oai.Operation] = getattr(path_data, method)
6464
if operation is None:
6565
continue
66-
tag = utils.PythonIdentifier(value=(operation.tags or ["default"])[0], prefix="tag")
67-
collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag))
66+
67+
tags = [utils.PythonIdentifier(value=tag, prefix="tag") for tag in operation.tags or ["default"]]
68+
if not config.generate_all_tags:
69+
tags = tags[:1]
70+
71+
collections = [endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag)) for tag in tags]
72+
6873
endpoint, schemas, parameters = Endpoint.from_data(
6974
data=operation,
7075
path=path,
7176
method=method,
72-
tag=tag,
77+
tags=tags,
7378
schemas=schemas,
7479
parameters=parameters,
7580
request_bodies=request_bodies,
@@ -87,15 +92,16 @@ def from_data(
8792
if not isinstance(endpoint, ParseError):
8893
endpoint = Endpoint.sort_parameters(endpoint=endpoint)
8994
if isinstance(endpoint, ParseError):
90-
endpoint.header = (
91-
f"WARNING parsing {method.upper()} {path} within {tag}. Endpoint will not be generated."
92-
)
93-
collection.parse_errors.append(endpoint)
95+
endpoint.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}. Endpoint will not be generated."
96+
for collection in collections:
97+
collection.parse_errors.append(endpoint)
9498
continue
9599
for error in endpoint.errors:
96-
error.header = f"WARNING parsing {method.upper()} {path} within {tag}."
97-
collection.parse_errors.append(error)
98-
collection.endpoints.append(endpoint)
100+
error.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}."
101+
for collection in collections:
102+
collection.parse_errors.append(error)
103+
for collection in collections:
104+
collection.endpoints.append(endpoint)
99105

100106
return endpoints_by_tag, schemas, parameters
101107

@@ -132,7 +138,7 @@ class Endpoint:
132138
description: Optional[str]
133139
name: str
134140
requires_security: bool
135-
tag: str
141+
tags: list[PythonIdentifier]
136142
summary: Optional[str] = ""
137143
relative_imports: set[str] = field(default_factory=set)
138144
query_parameters: list[Property] = field(default_factory=list)
@@ -393,7 +399,7 @@ def from_data(
393399
data: oai.Operation,
394400
path: str,
395401
method: str,
396-
tag: str,
402+
tags: list[PythonIdentifier],
397403
schemas: Schemas,
398404
parameters: Parameters,
399405
request_bodies: dict[str, Union[oai.RequestBody, oai.Reference]],
@@ -413,7 +419,7 @@ def from_data(
413419
description=utils.remove_string_escapes(data.description) if data.description else "",
414420
name=name,
415421
requires_security=bool(data.security),
416-
tag=tag,
422+
tags=tags,
417423
)
418424

419425
result, schemas, parameters = Endpoint.add_parameters(

0 commit comments

Comments
 (0)