Skip to content

Commit 3cd8a65

Browse files
authored
Support custom templates (#231)
Add --custom-template-path option to provide a directory to templates which override the defaults.
1 parent bccaabe commit 3cd8a65

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1188
-25
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ dmypy.json
1919
# JetBrains
2020
.idea/
2121

22+
test-reports/
23+
2224
/coverage.xml
2325
/.coverage
2426
htmlcov/
2527

2628
# Generated end to end test data
27-
my-test-api-client
29+
my-test-api-client

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ get an error.
5454

5555
> For more usage details run `openapi-python-client --help` or read [usage](usage.md)
5656
57+
58+
### Using custom templates
59+
60+
This feature leverages Jinja2's [ChoiceLoader](https://jinja.palletsprojects.com/en/2.11.x/api/#jinja2.ChoiceLoader) and [FileSystemLoader](https://jinja.palletsprojects.com/en/2.11.x/api/#jinja2.FileSystemLoader). This means you do _not_ need to customize every template. Simply copy the template(s) you want to customize from [the default template directory](openapi_python_client/templates) to your own custom template directory (file names _must_ match exactly) and pass the template directory through the `custom_template_path` flag to the `generate` and `update` commands. For instance,
61+
62+
```
63+
openapi-python-client update \
64+
--url https://my.api.com/openapi.json \
65+
--custom-template-path=relative/path/to/mytemplates
66+
```
67+
68+
_Be forewarned, this is a beta-level feature in the sense that the API exposed in the templates is undocumented and unstable._
69+
5770
## What You Get
5871

5972
1. A `pyproject.toml` file with some basic metadata intended to be used with [Poetry].
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
__pycache__/
2+
build/
3+
dist/
4+
*.egg-info/
5+
.pytest_cache/
6+
7+
# pyenv
8+
.python-version
9+
10+
# Environments
11+
.env
12+
.venv
13+
14+
# mypy
15+
.mypy_cache/
16+
.dmypy.json
17+
dmypy.json
18+
19+
# JetBrains
20+
.idea/
21+
22+
/coverage.xml
23+
/.coverage
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# my-test-api-client
2+
A client library for accessing My Test API
3+
4+
## Usage
5+
First, create a client:
6+
7+
```python
8+
from my_test_api_client import Client
9+
10+
client = Client(base_url="https://api.example.com")
11+
```
12+
13+
If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead:
14+
15+
```python
16+
from my_test_api_client import AuthenticatedClient
17+
18+
client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
19+
```
20+
21+
Now call your endpoint and use your models:
22+
23+
```python
24+
from my_test_api_client.models import MyDataModel
25+
from my_test_api_client.api.my_tag import get_my_data_model
26+
27+
my_data: MyDataModel = get_my_data_model(client=client)
28+
```
29+
30+
Or do the same thing with an async version:
31+
32+
```python
33+
from my_test_api_client.models import MyDataModel
34+
from my_test_api_client.async_api.my_tag import get_my_data_model
35+
36+
my_data: MyDataModel = await get_my_data_model(client=client)
37+
```
38+
39+
Things to know:
40+
1. Every path/method combo becomes a Python function with type annotations.
41+
1. All path/query params, and bodies become method arguments.
42+
1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above)
43+
1. Any endpoint which did not have a tag will be in `my_test_api_client.api.default`
44+
1. If the API returns a response code that was not declared in the OpenAPI document, a
45+
`my_test_api_client.api.errors.ApiResponseError` wil be raised
46+
with the `response` attribute set to the `httpx.Response` that was received.
47+
48+
49+
## Building / publishing this Client
50+
This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics:
51+
1. Update the metadata in pyproject.toml (e.g. authors, version)
52+
1. If you're using a private repository, configure it with Poetry
53+
1. `poetry config repositories.<your-repository-name> <url-to-your-repository>`
54+
1. `poetry config http-basic.<your-repository-name> <username> <password>`
55+
1. Publish the client with `poetry publish --build -r <your-repository-name>` or, if for public PyPI, just `poetry publish --build`
56+
57+
If you want to install this client into another project without publishing it (e.g. for development) then:
58+
1. If that project **is using Poetry**, you can simply do `poetry add <path-to-this-client>` from that project
59+
1. If that project is not using Poetry:
60+
1. Build a wheel with `poetry build -f wheel`
61+
1. Install that wheel from the other project `pip install <path-to-wheel>`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
""" A client library for accessing My Test API """
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
""" Contains methods for accessing the API """

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

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Optional
2+
3+
import httpx
4+
5+
Client = httpx.Client
6+
7+
8+
def _parse_response(*, response: httpx.Response) -> Optional[bool]:
9+
if response.status_code == 200:
10+
return bool(response.text)
11+
return None
12+
13+
14+
def _build_response(*, response: httpx.Response) -> httpx.Response[bool]:
15+
return httpx.Response(
16+
status_code=response.status_code,
17+
content=response.content,
18+
headers=response.headers,
19+
parsed=_parse_response(response=response),
20+
)
21+
22+
23+
def httpx_request(
24+
*,
25+
client: Client,
26+
) -> httpx.Response[bool]:
27+
28+
response = client.request(
29+
"get",
30+
"/ping",
31+
)
32+
33+
return _build_response(response=response)

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

Whitespace-only changes.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from typing import Optional
2+
3+
import httpx
4+
5+
Client = httpx.Client
6+
7+
import datetime
8+
from typing import Dict, List, Optional, Union, cast
9+
10+
from dateutil.parser import isoparse
11+
12+
from ...models.an_enum import AnEnum
13+
from ...models.http_validation_error import HTTPValidationError
14+
15+
16+
def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]:
17+
if response.status_code == 200:
18+
return None
19+
if response.status_code == 422:
20+
return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json()))
21+
return None
22+
23+
24+
def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]:
25+
return httpx.Response(
26+
status_code=response.status_code,
27+
content=response.content,
28+
headers=response.headers,
29+
parsed=_parse_response(response=response),
30+
)
31+
32+
33+
def httpx_request(
34+
*,
35+
client: Client,
36+
json_body: Dict[Any, Any],
37+
string_prop: Optional[str] = "the default string",
38+
datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"),
39+
date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(),
40+
float_prop: Optional[float] = 3.14,
41+
int_prop: Optional[int] = 7,
42+
boolean_prop: Optional[bool] = False,
43+
list_prop: Optional[List[AnEnum]] = None,
44+
union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float",
45+
enum_prop: Optional[AnEnum] = None,
46+
) -> httpx.Response[Union[None, HTTPValidationError]]:
47+
48+
json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None
49+
50+
json_date_prop = date_prop.isoformat() if date_prop else None
51+
52+
if list_prop is None:
53+
json_list_prop = None
54+
else:
55+
json_list_prop = []
56+
for list_prop_item_data in list_prop:
57+
list_prop_item = list_prop_item_data.value
58+
59+
json_list_prop.append(list_prop_item)
60+
61+
if union_prop is None:
62+
json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None
63+
elif isinstance(union_prop, float):
64+
json_union_prop = union_prop
65+
else:
66+
json_union_prop = union_prop
67+
68+
json_enum_prop = enum_prop.value if enum_prop else None
69+
70+
params: Dict[str, Any] = {}
71+
if string_prop is not None:
72+
params["string_prop"] = string_prop
73+
if datetime_prop is not None:
74+
params["datetime_prop"] = json_datetime_prop
75+
if date_prop is not None:
76+
params["date_prop"] = json_date_prop
77+
if float_prop is not None:
78+
params["float_prop"] = float_prop
79+
if int_prop is not None:
80+
params["int_prop"] = int_prop
81+
if boolean_prop is not None:
82+
params["boolean_prop"] = boolean_prop
83+
if list_prop is not None:
84+
params["list_prop"] = json_list_prop
85+
if union_prop is not None:
86+
params["union_prop"] = json_union_prop
87+
if enum_prop is not None:
88+
params["enum_prop"] = json_enum_prop
89+
90+
json_json_body = json_body
91+
92+
response = client.request(
93+
"post",
94+
"/tests/defaults",
95+
json=json_json_body,
96+
params=params,
97+
)
98+
99+
return _build_response(response=response)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Optional
2+
3+
import httpx
4+
5+
Client = httpx.Client
6+
7+
8+
def _parse_response(*, response: httpx.Response) -> Optional[List[bool]]:
9+
if response.status_code == 200:
10+
return [bool(item) for item in cast(List[bool], response.json())]
11+
return None
12+
13+
14+
def _build_response(*, response: httpx.Response) -> httpx.Response[List[bool]]:
15+
return httpx.Response(
16+
status_code=response.status_code,
17+
content=response.content,
18+
headers=response.headers,
19+
parsed=_parse_response(response=response),
20+
)
21+
22+
23+
def httpx_request(
24+
*,
25+
client: Client,
26+
) -> httpx.Response[List[bool]]:
27+
28+
response = client.request(
29+
"get",
30+
"/tests/basic_lists/booleans",
31+
)
32+
33+
return _build_response(response=response)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Optional
2+
3+
import httpx
4+
5+
Client = httpx.Client
6+
7+
8+
def _parse_response(*, response: httpx.Response) -> Optional[List[float]]:
9+
if response.status_code == 200:
10+
return [float(item) for item in cast(List[float], response.json())]
11+
return None
12+
13+
14+
def _build_response(*, response: httpx.Response) -> httpx.Response[List[float]]:
15+
return httpx.Response(
16+
status_code=response.status_code,
17+
content=response.content,
18+
headers=response.headers,
19+
parsed=_parse_response(response=response),
20+
)
21+
22+
23+
def httpx_request(
24+
*,
25+
client: Client,
26+
) -> httpx.Response[List[float]]:
27+
28+
response = client.request(
29+
"get",
30+
"/tests/basic_lists/floats",
31+
)
32+
33+
return _build_response(response=response)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Optional
2+
3+
import httpx
4+
5+
Client = httpx.Client
6+
7+
8+
def _parse_response(*, response: httpx.Response) -> Optional[List[int]]:
9+
if response.status_code == 200:
10+
return [int(item) for item in cast(List[int], response.json())]
11+
return None
12+
13+
14+
def _build_response(*, response: httpx.Response) -> httpx.Response[List[int]]:
15+
return httpx.Response(
16+
status_code=response.status_code,
17+
content=response.content,
18+
headers=response.headers,
19+
parsed=_parse_response(response=response),
20+
)
21+
22+
23+
def httpx_request(
24+
*,
25+
client: Client,
26+
) -> httpx.Response[List[int]]:
27+
28+
response = client.request(
29+
"get",
30+
"/tests/basic_lists/integers",
31+
)
32+
33+
return _build_response(response=response)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Optional
2+
3+
import httpx
4+
5+
Client = httpx.Client
6+
7+
8+
def _parse_response(*, response: httpx.Response) -> Optional[List[str]]:
9+
if response.status_code == 200:
10+
return [str(item) for item in cast(List[str], response.json())]
11+
return None
12+
13+
14+
def _build_response(*, response: httpx.Response) -> httpx.Response[List[str]]:
15+
return httpx.Response(
16+
status_code=response.status_code,
17+
content=response.content,
18+
headers=response.headers,
19+
parsed=_parse_response(response=response),
20+
)
21+
22+
23+
def httpx_request(
24+
*,
25+
client: Client,
26+
) -> httpx.Response[List[str]]:
27+
28+
response = client.request(
29+
"get",
30+
"/tests/basic_lists/strings",
31+
)
32+
33+
return _build_response(response=response)

0 commit comments

Comments
 (0)