Skip to content

Commit bc855b3

Browse files
dependabot-preview[bot]fyhertz
authored andcommitted
Add 0 boilerplate sync and async client objects
With the current approach, for every API call, the user has to import a a module from a tag package and call the `asyncio` or `sync` method with the "client=client" argument. This patch adds a wrapper around tag packages and two wrapper `Client` objects to spare the need for that boilerplate code. Check this issue for more information: openapi-generators#224 `base_url` can be omitted during client initialization if `SPEC_TITLE_BASE_URL` is set. Object-oriented clients have also been renamed to include the spec title.
1 parent c8fa981 commit bc855b3

File tree

8 files changed

+150
-11
lines changed

8 files changed

+150
-11
lines changed

openapi_python_client/__init__.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def __init__(self, *, openapi: GeneratorData, custom_template_path: Optional[Pat
5555
self.package_description: str = utils.remove_string_escapes(
5656
f"A client library for accessing {self.openapi.title}"
5757
)
58+
self.client_name: str = f'{utils.pascal_case(openapi.title)}Client'
5859
self.version: str = self.package_version_override or openapi.version
59-
6060
self.env.filters.update(self.TEMPLATE_FILTERS)
6161

6262
def build(self) -> Sequence[GeneratorError]:
@@ -117,7 +117,9 @@ def _create_package(self) -> None:
117117
package_init = self.package_dir / "__init__.py"
118118

119119
package_init_template = self.env.get_template("package_init.pyi")
120-
package_init.write_text(package_init_template.render(description=self.package_description))
120+
package_init.write_text(
121+
package_init_template.render(client_name=self.client_name, description=self.package_description)
122+
)
121123

122124
pytyped = self.package_dir / "py.typed"
123125
pytyped.write_text("# Marker file for PEP 561")
@@ -184,7 +186,20 @@ def _build_api(self) -> None:
184186
# Generate Client
185187
client_path = self.package_dir / "client.py"
186188
client_template = self.env.get_template("client.pyi")
187-
client_path.write_text(client_template.render())
189+
client_path.write_text(client_template.render(title=self.openapi.title))
190+
191+
# Generate wrapper
192+
imports = [m.reference.class_name for m in self.openapi.models.values()]
193+
imports.extend([e.reference.class_name for e in self.openapi.enums.values()])
194+
wrapper = self.package_dir / "wrapper.py"
195+
wrapper_template = self.env.get_template("wrapper.pyi")
196+
wrapper.write_text(
197+
wrapper_template.render(
198+
client_name=self.client_name,
199+
imports=imports,
200+
endpoint_collections=self.openapi.endpoint_collections_by_tag,
201+
)
202+
)
188203

189204
# Generate endpoints
190205
api_dir = self.package_dir / "api"
@@ -197,7 +212,9 @@ def _build_api(self) -> None:
197212
tag = utils.snake_case(tag)
198213
tag_dir = api_dir / tag
199214
tag_dir.mkdir()
200-
(tag_dir / "__init__.py").touch()
215+
tag_init = tag_dir / "__init__.py"
216+
tag_init_template = self.env.get_template("tag_init.pyi")
217+
tag_init.write_text(tag_init_template.render(tag=tag, collection=collection))
201218

202219
for endpoint in collection.endpoints:
203220
module_path = tag_dir / f"{snake_case(endpoint.name)}.py"

openapi_python_client/templates/client.pyi

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1-
from typing import Dict
1+
import os
2+
from typing import Dict, Optional
23

34
import attr
45

56
@attr.s(auto_attribs=True)
67
class Client:
78
""" A class for keeping track of data related to the API """
89

9-
base_url: str
10+
base_url: Optional[str] = attr.ib(None, kw_only=True)
1011
cookies: Dict[str, str] = attr.ib(factory=dict, kw_only=True)
1112
headers: Dict[str, str] = attr.ib(factory=dict, kw_only=True)
1213
timeout: float = attr.ib(5.0, kw_only=True)
1314

15+
def __attrs_post_init__(self):
16+
env_base_url = os.environ.get('{{ title | snakecase | upper }}_BASE_URL')
17+
self.base_url = self.base_url or env_base_url
18+
if self.base_url is None:
19+
raise ValueError(f'"base_url" has to be set either from the '
20+
f'environment variable "{env_base_url}", or '
21+
f'passed with the "base_url" argument')
22+
1423
def get_headers(self) -> Dict[str, str]:
1524
""" Get headers to be used in all endpoints """
1625
return {**self.headers}

openapi_python_client/templates/endpoint_macros.pyi

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,18 @@ Union[
7070
{% endmacro %}
7171

7272
{# The all the kwargs passed into an endpoint (and variants thereof)) #}
73-
{% macro arguments(endpoint) %}
73+
{% macro arguments(endpoint, client=True) %}
74+
{% if endpoint.path_parameters or endpoint.form_body_reference or endpoint.multipart_body_reference or endpoint.query_parameters or endpoint.json_body or endpoint.header_parameters %}
7475
*,
76+
{% endif %}
7577
{# Proper client based on whether or not the endpoint requires authentication #}
78+
{% if client %}
7679
{% if endpoint.requires_security %}
7780
client: AuthenticatedClient,
7881
{% else %}
7982
client: Client,
8083
{% endif %}
84+
{% endif %}
8185
{# path parameters #}
8286
{% for parameter in endpoint.path_parameters %}
8387
{{ parameter.to_string() }},
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
""" {{ description }} """
2-
from .client import AuthenticatedClient, Client
2+
3+
from .wrapper import Sync{{ client_name }}, {{ client_name }}

openapi_python_client/templates/property_templates/union_property.pyi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ def _parse_{{ property.python_name }}(data: Any) -> {{ property.get_type_string(
1919
{% endif %}
2020
{% endfor %}
2121

22+
{% if not property.nullable %}
2223
{{ property.python_name }} = _parse_{{ property.python_name }}({{ source }})
24+
{% else %}
25+
{{ property.python_name }} = None
26+
if {{ source }} is not None:
27+
{{ property.python_name }} = _parse_{{ property.python_name }}({{ source }})
28+
{% endif %}
29+
2330
{% endmacro %}
2431

2532
{% macro transform(property, source, destination, declare_type=True) %}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import {% for e in collection.endpoints %} {{e.name | snakecase }}, {% endfor %}
2+
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from typing import Any, Dict, Optional, Union, cast, List
2+
from .client import Client as InnerClient, AuthenticatedClient
3+
from .types import UNSET, Unset
4+
5+
6+
from .models import (
7+
{% for import in imports | sort %}
8+
{{ import }},
9+
{% endfor %}
10+
)
11+
12+
from .api import (
13+
{% for tag, collection in endpoint_collections.items() %}
14+
{{ tag | snakecase }},
15+
{% endfor %}
16+
)
17+
18+
{% from "endpoint_macros.pyi" import arguments, client, kwargs %}
19+
20+
{% for tag, collection in endpoint_collections.items() %}
21+
22+
class {{ tag | pascalcase }}Api:
23+
24+
def __init__(self, client: InnerClient):
25+
self._client = client
26+
27+
{% for endpoint in collection.endpoints %}
28+
async def {{ endpoint.name | snakecase }}(
29+
self,
30+
{{ arguments(endpoint, False) | indent(8) }}
31+
):
32+
{% if endpoint.requires_security %}
33+
client = cast(AuthenticatedClient, self._client)
34+
{% else %}
35+
client = self._client
36+
{% endif %}
37+
return await {{ tag }}.{{ endpoint.name | snakecase }}.asyncio(
38+
{{ kwargs(endpoint) | indent(12) }}
39+
)
40+
41+
{% endfor %}
42+
43+
44+
class Sync{{ tag | pascalcase }}Api:
45+
46+
def __init__(self, client: InnerClient):
47+
self._client = client
48+
49+
{% for endpoint in collection.endpoints %}
50+
def {{ endpoint.name | snakecase }}(
51+
self,
52+
{{ arguments(endpoint, False) | indent(8) }}
53+
):
54+
{% if endpoint.requires_security %}
55+
client = cast(AuthenticatedClient, self._client)
56+
{% else %}
57+
client = self._client
58+
{% endif %}
59+
return {{ tag }}.{{ endpoint.name | snakecase }}.sync(
60+
{{ kwargs(endpoint) | indent(12) }}
61+
)
62+
63+
{% endfor %}
64+
65+
{% endfor %}
66+
67+
{% for prefix in '', 'Sync' %}
68+
69+
class {{ prefix }}{{ client_name }}:
70+
def __init__(self, base_url: Optional[str] = None, timeout: float = 5.0, token: Optional[str] = None):
71+
if token is None:
72+
self.connection = InnerClient(
73+
base_url=base_url,
74+
timeout=timeout)
75+
else:
76+
self.connection = AuthenticatedClient(
77+
base_url=base_url,
78+
timeout=timeout,
79+
token=token)
80+
{% for tag, collection in endpoint_collections.items() %}
81+
self.{{ tag | snakecase }} = {{ prefix }}{{ tag | pascalcase }}Api(self.connection)
82+
{% endfor %}
83+
84+
{% endfor %}

poetry.lock

Lines changed: 18 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)