Skip to content

support custom template path #231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Nov 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ dmypy.json
# JetBrains
.idea/

test-reports/

/coverage.xml
/.coverage
htmlcov/

# Generated end to end test data
my-test-api-client
my-test-api-client
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ get an error.

> For more usage details run `openapi-python-client --help` or read [usage](usage.md)


### Using custom templates

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,

```
openapi-python-client update \
--url https://my.api.com/openapi.json \
--custom-template-path=relative/path/to/mytemplates
```

_Be forewarned, this is a beta-level feature in the sense that the API exposed in the templates is undocumented and unstable._

## What You Get

1. A `pyproject.toml` file with some basic metadata intended to be used with [Poetry].
Expand Down
23 changes: 23 additions & 0 deletions end_to_end_tests/golden-record-custom/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
__pycache__/
build/
dist/
*.egg-info/
.pytest_cache/

# pyenv
.python-version

# Environments
.env
.venv

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# JetBrains
.idea/

/coverage.xml
/.coverage
61 changes: 61 additions & 0 deletions end_to_end_tests/golden-record-custom/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# my-test-api-client
A client library for accessing My Test API

## Usage
First, create a client:

```python
from my_test_api_client import Client

client = Client(base_url="https://api.example.com")
```

If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead:

```python
from my_test_api_client import AuthenticatedClient

client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
```

Now call your endpoint and use your models:

```python
from my_test_api_client.models import MyDataModel
from my_test_api_client.api.my_tag import get_my_data_model

my_data: MyDataModel = get_my_data_model(client=client)
```

Or do the same thing with an async version:

```python
from my_test_api_client.models import MyDataModel
from my_test_api_client.async_api.my_tag import get_my_data_model

my_data: MyDataModel = await get_my_data_model(client=client)
```

Things to know:
1. Every path/method combo becomes a Python function with type annotations.
1. All path/query params, and bodies become method arguments.
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)
1. Any endpoint which did not have a tag will be in `my_test_api_client.api.default`
1. If the API returns a response code that was not declared in the OpenAPI document, a
`my_test_api_client.api.errors.ApiResponseError` wil be raised
with the `response` attribute set to the `httpx.Response` that was received.


## Building / publishing this Client
This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics:
1. Update the metadata in pyproject.toml (e.g. authors, version)
1. If you're using a private repository, configure it with Poetry
1. `poetry config repositories.<your-repository-name> <url-to-your-repository>`
1. `poetry config http-basic.<your-repository-name> <username> <password>`
1. Publish the client with `poetry publish --build -r <your-repository-name>` or, if for public PyPI, just `poetry publish --build`

If you want to install this client into another project without publishing it (e.g. for development) then:
1. If that project **is using Poetry**, you can simply do `poetry add <path-to-this-client>` from that project
1. If that project is not using Poetry:
1. Build a wheel with `poetry build -f wheel`
1. Install that wheel from the other project `pip install <path-to-wheel>`
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
""" A client library for accessing My Test API """
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
""" Contains methods for accessing the API """
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Optional

import httpx

Client = httpx.Client


def _parse_response(*, response: httpx.Response) -> Optional[bool]:
if response.status_code == 200:
return bool(response.text)
return None


def _build_response(*, response: httpx.Response) -> httpx.Response[bool]:
return httpx.Response(
status_code=response.status_code,
content=response.content,
headers=response.headers,
parsed=_parse_response(response=response),
)


def httpx_request(
*,
client: Client,
) -> httpx.Response[bool]:

response = client.request(
"get",
"/ping",
)

return _build_response(response=response)
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from typing import Optional

import httpx

Client = httpx.Client

import datetime
from typing import Dict, List, Optional, Union, cast

from dateutil.parser import isoparse

from ...models.an_enum import AnEnum
from ...models.http_validation_error import HTTPValidationError


def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]:
if response.status_code == 200:
return None
if response.status_code == 422:
return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json()))
return None


def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]:
return httpx.Response(
status_code=response.status_code,
content=response.content,
headers=response.headers,
parsed=_parse_response(response=response),
)


def httpx_request(
*,
client: Client,
json_body: Dict[Any, Any],
string_prop: Optional[str] = "the default string",
datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"),
date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(),
float_prop: Optional[float] = 3.14,
int_prop: Optional[int] = 7,
boolean_prop: Optional[bool] = False,
list_prop: Optional[List[AnEnum]] = None,
union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float",
enum_prop: Optional[AnEnum] = None,
) -> httpx.Response[Union[None, HTTPValidationError]]:

json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None

json_date_prop = date_prop.isoformat() if date_prop else None

if list_prop is None:
json_list_prop = None
else:
json_list_prop = []
for list_prop_item_data in list_prop:
list_prop_item = list_prop_item_data.value

json_list_prop.append(list_prop_item)

if union_prop is None:
json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None
elif isinstance(union_prop, float):
json_union_prop = union_prop
else:
json_union_prop = union_prop

json_enum_prop = enum_prop.value if enum_prop else None

params: Dict[str, Any] = {}
if string_prop is not None:
params["string_prop"] = string_prop
if datetime_prop is not None:
params["datetime_prop"] = json_datetime_prop
if date_prop is not None:
params["date_prop"] = json_date_prop
if float_prop is not None:
params["float_prop"] = float_prop
if int_prop is not None:
params["int_prop"] = int_prop
if boolean_prop is not None:
params["boolean_prop"] = boolean_prop
if list_prop is not None:
params["list_prop"] = json_list_prop
if union_prop is not None:
params["union_prop"] = json_union_prop
if enum_prop is not None:
params["enum_prop"] = json_enum_prop

json_json_body = json_body

response = client.request(
"post",
"/tests/defaults",
json=json_json_body,
params=params,
)

return _build_response(response=response)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Optional

import httpx

Client = httpx.Client


def _parse_response(*, response: httpx.Response) -> Optional[List[bool]]:
if response.status_code == 200:
return [bool(item) for item in cast(List[bool], response.json())]
return None


def _build_response(*, response: httpx.Response) -> httpx.Response[List[bool]]:
return httpx.Response(
status_code=response.status_code,
content=response.content,
headers=response.headers,
parsed=_parse_response(response=response),
)


def httpx_request(
*,
client: Client,
) -> httpx.Response[List[bool]]:

response = client.request(
"get",
"/tests/basic_lists/booleans",
)

return _build_response(response=response)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Optional

import httpx

Client = httpx.Client


def _parse_response(*, response: httpx.Response) -> Optional[List[float]]:
if response.status_code == 200:
return [float(item) for item in cast(List[float], response.json())]
return None


def _build_response(*, response: httpx.Response) -> httpx.Response[List[float]]:
return httpx.Response(
status_code=response.status_code,
content=response.content,
headers=response.headers,
parsed=_parse_response(response=response),
)


def httpx_request(
*,
client: Client,
) -> httpx.Response[List[float]]:

response = client.request(
"get",
"/tests/basic_lists/floats",
)

return _build_response(response=response)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Optional

import httpx

Client = httpx.Client


def _parse_response(*, response: httpx.Response) -> Optional[List[int]]:
if response.status_code == 200:
return [int(item) for item in cast(List[int], response.json())]
return None


def _build_response(*, response: httpx.Response) -> httpx.Response[List[int]]:
return httpx.Response(
status_code=response.status_code,
content=response.content,
headers=response.headers,
parsed=_parse_response(response=response),
)


def httpx_request(
*,
client: Client,
) -> httpx.Response[List[int]]:

response = client.request(
"get",
"/tests/basic_lists/integers",
)

return _build_response(response=response)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Optional

import httpx

Client = httpx.Client


def _parse_response(*, response: httpx.Response) -> Optional[List[str]]:
if response.status_code == 200:
return [str(item) for item in cast(List[str], response.json())]
return None


def _build_response(*, response: httpx.Response) -> httpx.Response[List[str]]:
return httpx.Response(
status_code=response.status_code,
content=response.content,
headers=response.headers,
parsed=_parse_response(response=response),
)


def httpx_request(
*,
client: Client,
) -> httpx.Response[List[str]]:

response = client.request(
"get",
"/tests/basic_lists/strings",
)

return _build_response(response=response)
Loading