Skip to content

Commit 84c8c23

Browse files
committed
Merge branch 'master' into pr/31
2 parents 640d111 + 66970cb commit 84c8c23

34 files changed

+445
-130
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## 0.3.0 - Unreleased
8+
### Additions
9+
- Link to the GitHub repository from PyPI (#26). Thanks @theY4Kman!
10+
11+
### Fixes
12+
- Fixed some typing issues in generated clients and incorporate mypy into end to end tests (#32). Thanks @acgray!
13+
- Properly handle camelCase endpoint names and properties (#29, #36). Thanks @acgray!
14+
715
## 0.2.1 - 2020-03-22
816
### Fixes
917
- Fixed import of errors.py in generated api modules

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,17 @@ You can pass a YAML (or JSON) file to openapi-python-client in order to change s
6767
are supported:
6868

6969
### class_overrides
70-
Used to change the name of generated model classes, especially useful if you have a name like ABCModel which, when
71-
converted to snake case for module naming will be a_b_c_model. This param should be a mapping of existing class name
72-
(usually a key in the "schemas" section of your OpenAPI document) to class_name and module_name.
70+
Used to change the name of generated model classes. This param should be a mapping of existing class name
71+
(usually a key in the "schemas" section of your OpenAPI document) to class_name and module_name. As an example, if the
72+
name of the a model in OpenAPI (and therefore the generated class name) was something like "_PrivateInternalLongName"
73+
and you want the generated client's model to be called "ShortName" in a module called "short_name" you could do this:
7374

7475
Example:
7576
```yaml
7677
class_overrides:
77-
ABCModel:
78-
class_name: ABCModel
79-
module_name: abc_model
78+
_PrivateInternalLongName:
79+
class_name: ShortName
80+
module_name: short_name
8081
```
8182
8283
The easiest way to find what needs to be overridden is probably to generate your client and go look at everything in the

openapi_python_client/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import yaml
1313
from jinja2 import Environment, PackageLoader
1414

15+
from openapi_python_client import utils
16+
1517
from .openapi_parser import OpenAPI, import_string_from_reference
1618

1719
__version__ = version(__package__)
@@ -61,6 +63,8 @@ def _get_json(*, url: Optional[str], path: Optional[Path]) -> Dict[str, Any]:
6163

6264

6365
class _Project:
66+
TEMPLATE_FILTERS = {"snakecase": utils.snake_case}
67+
6468
def __init__(self, *, openapi: OpenAPI) -> None:
6569
self.openapi: OpenAPI = openapi
6670
self.env: Environment = Environment(loader=PackageLoader(__package__), trim_blocks=True, lstrip_blocks=True)
@@ -72,6 +76,8 @@ def __init__(self, *, openapi: OpenAPI) -> None:
7276
self.package_dir: Path = self.project_dir / self.package_name
7377
self.package_description = f"A client library for accessing {self.openapi.title}"
7478

79+
self.env.filters.update(self.TEMPLATE_FILTERS)
80+
7581
def build(self) -> None:
7682
""" Create the project from templates """
7783

openapi_python_client/openapi_parser/openapi.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,10 @@ def dict(d: Dict[str, Dict[str, Any]], /) -> Dict[str, Schema]:
199199

200200
@dataclass
201201
class OpenAPI:
202-
""" Top level OpenAPI spec """
202+
""" Top level OpenAPI document """
203203

204204
title: str
205-
description: str
205+
description: Optional[str]
206206
version: str
207207
schemas: Dict[str, Schema]
208208
endpoint_collections_by_tag: Dict[str, EndpointCollection]
@@ -251,7 +251,7 @@ def from_dict(d: Dict[str, Dict[str, Any]], /) -> OpenAPI:
251251

252252
return OpenAPI(
253253
title=d["info"]["title"],
254-
description=d["info"]["description"],
254+
description=d["info"].get("description"),
255255
version=d["info"]["version"],
256256
endpoint_collections_by_tag=endpoint_collections_by_tag,
257257
schemas=schemas,

openapi_python_client/openapi_parser/properties.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from dataclasses import dataclass, field
22
from typing import Any, ClassVar, Dict, List, Optional
33

4+
from openapi_python_client import utils
5+
46
from .reference import Reference
57

68

@@ -15,6 +17,11 @@ class Property:
1517
constructor_template: ClassVar[Optional[str]] = None
1618
_type_string: ClassVar[str]
1719

20+
python_name: str = field(init=False)
21+
22+
def __post_init__(self) -> None:
23+
self.python_name = utils.snake_case(self.name)
24+
1825
def get_type_string(self) -> str:
1926
""" Get a string representation of type that should be used when declaring this property """
2027
if self.required:
@@ -31,13 +38,13 @@ def to_string(self) -> str:
3138
default = None
3239

3340
if default is not None:
34-
return f"{self.name}: {self.get_type_string()} = {self.default}"
41+
return f"{self.python_name}: {self.get_type_string()} = {self.default}"
3542
else:
36-
return f"{self.name}: {self.get_type_string()}"
43+
return f"{self.python_name}: {self.get_type_string()}"
3744

3845
def transform(self) -> str:
3946
""" What it takes to turn this object into a native python type """
40-
return self.name
47+
return self.python_name
4148

4249
def constructor_from_dict(self, dict_name: str) -> str:
4350
""" How to load this property from a dict (used in generated model from_dict function """
@@ -57,6 +64,7 @@ class StringProperty(Property):
5764
_type_string: ClassVar[str] = "str"
5865

5966
def __post_init__(self) -> None:
67+
super().__post_init__()
6068
if self.default is not None:
6169
self.default = f'"{self.default}"'
6270

@@ -132,6 +140,7 @@ class EnumListProperty(Property):
132140
constructor_template: ClassVar[str] = "enum_list_property.pyi"
133141

134142
def __post_init__(self) -> None:
143+
super().__post_init__()
135144
self.reference = Reference.from_ref(self.name)
136145

137146
def get_type_string(self) -> str:
@@ -149,6 +158,7 @@ class EnumProperty(Property):
149158
reference: Reference
150159

151160
def __post_init__(self) -> None:
161+
super().__post_init__()
152162
inverse_values = {v: k for k, v in self.values.items()}
153163
if self.default is not None:
154164
self.default = f"{self.reference.class_name}.{inverse_values[self.default]}"
@@ -162,11 +172,14 @@ def get_type_string(self) -> str:
162172

163173
def transform(self) -> str:
164174
""" Output to the template, convert this Enum into a JSONable value """
165-
return f"{self.name}.value"
175+
return f"{self.python_name}.value"
166176

167177
def constructor_from_dict(self, dict_name: str) -> str:
168178
""" How to load this property from a dict (used in generated model from_dict function """
169-
return f'{self.reference.class_name}({dict_name}["{self.name}"]) if "{self.name}" in {dict_name} else None'
179+
constructor = f'{self.reference.class_name}({dict_name}["{self.name}"])'
180+
if not self.required:
181+
constructor += f' if "{self.name}" in {dict_name} else None'
182+
return constructor
170183

171184
@staticmethod
172185
def values_from_list(l: List[str], /) -> Dict[str, str]:
@@ -200,22 +213,22 @@ def get_type_string(self) -> str:
200213

201214
def transform(self) -> str:
202215
""" Convert this into a JSONable value """
203-
return f"{self.name}.to_dict()"
216+
return f"{self.python_name}.to_dict()"
204217

205218

206219
@dataclass
207220
class DictProperty(Property):
208221
""" Property that is a general Dict """
209222

210-
_type_string: ClassVar[str] = "Dict"
223+
_type_string: ClassVar[str] = "Dict[Any, Any]"
211224

212225

213226
_openapi_types_to_python_type_strings = {
214227
"string": "str",
215228
"number": "float",
216229
"integer": "int",
217230
"boolean": "bool",
218-
"object": "Dict",
231+
"object": "Dict[Any, Any]",
219232
}
220233

221234

openapi_python_client/openapi_parser/reference.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dataclasses import dataclass
66
from typing import Dict
77

8-
import stringcase
8+
from .. import utils
99

1010
class_overrides: Dict[str, Reference] = {}
1111

@@ -22,9 +22,9 @@ def from_ref(ref: str) -> Reference:
2222
""" Get a Reference from the openapi #/schemas/blahblah string """
2323
ref_value = ref.split("/")[-1]
2424
# ugly hack to avoid stringcase ugly pascalcase output when ref_value isn't snake case
25-
class_name = stringcase.pascalcase(ref_value.replace(" ", ""))
25+
class_name = utils.pascal_case(ref_value.replace(" ", ""))
2626

2727
if class_name in class_overrides:
2828
return class_overrides[class_name]
2929

30-
return Reference(class_name=class_name, module_name=stringcase.snakecase(class_name),)
30+
return Reference(class_name=class_name, module_name=utils.snake_case(class_name))

openapi_python_client/openapi_parser/responses.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def return_string(self) -> str:
3333

3434
def constructor(self) -> str:
3535
""" How the return value of this response should be constructed """
36-
return f"[{self.reference.class_name}.from_dict(item) for item in response.json()]"
36+
return f"[{self.reference.class_name}.from_dict(item) for item in cast(List[Dict[str, Any]], response.json())]"
3737

3838

3939
@dataclass
@@ -48,7 +48,7 @@ def return_string(self) -> str:
4848

4949
def constructor(self) -> str:
5050
""" How the return value of this response should be constructed """
51-
return f"{self.reference.class_name}.from_dict(response.json())"
51+
return f"{self.reference.class_name}.from_dict(cast(Dict[str, Any], response.json()))"
5252

5353

5454
@dataclass

openapi_python_client/templates/async_endpoint_module.pyi

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import asdict
2-
from typing import Dict, List, Optional, Union
2+
from typing import Any, Dict, List, Optional, Union, cast
33

44
import httpx
55

@@ -12,7 +12,7 @@ from ..errors import ApiResponseError
1212
{% for endpoint in collection.endpoints %}
1313

1414

15-
async def {{ endpoint.name }}(
15+
async def {{ endpoint.name | snakecase }}(
1616
*,
1717
{# Proper client based on whether or not the endpoint requires authentication #}
1818
{% if endpoint.requires_security %}
@@ -42,7 +42,12 @@ async def {{ endpoint.name }}(
4242
{% endfor %}
4343
]:
4444
""" {{ endpoint.description }} """
45-
url = f"{client.base_url}{{ endpoint.path }}"
45+
url = "{}{{ endpoint.path }}".format(
46+
client.base_url
47+
{%- for parameter in endpoint.path_parameters -%}
48+
,{{parameter.name}}={{parameter.python_name}}
49+
{%- endfor -%}
50+
)
4651

4752
{% if endpoint.query_parameters %}
4853
params = {
@@ -54,14 +59,14 @@ async def {{ endpoint.name }}(
5459
}
5560
{% for parameter in endpoint.query_parameters %}
5661
{% if not parameter.required %}
57-
if {{ parameter.name }} is not None:
58-
params["{{ parameter.name }}"] = {{ parameter.transform() }}
62+
if {{ parameter.python_name }} is not None:
63+
params["{{ parameter.name }}"] = str({{ parameter.transform() }})
5964
{% endif %}
6065
{% endfor %}
6166
{% endif %}
6267

63-
with httpx.AsyncClient() as client:
64-
response = await client.{{ endpoint.method }}(
68+
async with httpx.AsyncClient() as _client:
69+
response = await _client.{{ endpoint.method }}(
6570
url=url,
6671
headers=client.get_headers(),
6772
{% if endpoint.form_body_reference %}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% if property.required %}
2-
{{ property.name }} = datetime.fromisoformat(d["{{ property.name }}"])
2+
{{ property.python_name }} = datetime.fromisoformat(d["{{ property.name }}"])
33
{% else %}
4-
{{ property.name }} = None
5-
if ({{ property.name }}_string := d.get("{{ property.name }}")) is not None:
6-
{{ property.name }} = datetime.fromisoformat(cast(str, {{ property.name }}_string))
4+
{{ property.python_name }} = None
5+
if ({{ property.python_name }}_string := d.get("{{ property.name }}")) is not None:
6+
{{ property.python_name }} = datetime.fromisoformat(cast(str, {{ property.python_name }}_string))
77
{% endif %}

openapi_python_client/templates/endpoint_module.pyi

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import asdict
2-
from typing import Dict, List, Optional, Union
2+
from typing import Any, Dict, List, Optional, Union, cast
33

44
import httpx
55

@@ -12,7 +12,7 @@ from ..errors import ApiResponseError
1212
{% for endpoint in collection.endpoints %}
1313

1414

15-
def {{ endpoint.name }}(
15+
def {{ endpoint.name | snakecase }}(
1616
*,
1717
{# Proper client based on whether or not the endpoint requires authentication #}
1818
{% if endpoint.requires_security %}
@@ -42,7 +42,12 @@ def {{ endpoint.name }}(
4242
{% endfor %}
4343
]:
4444
""" {{ endpoint.description }} """
45-
url = f"{client.base_url}{{ endpoint.path }}"
45+
url = "{}{{ endpoint.path }}".format(
46+
client.base_url
47+
{%- for parameter in endpoint.path_parameters -%}
48+
,{{parameter.name}}={{parameter.python_name}}
49+
{%- endfor -%}
50+
)
4651

4752
{% if endpoint.query_parameters %}
4853
params = {
@@ -54,8 +59,8 @@ def {{ endpoint.name }}(
5459
}
5560
{% for parameter in endpoint.query_parameters %}
5661
{% if not parameter.required %}
57-
if {{ parameter.name }} is not None:
58-
params["{{ parameter.name }}"] = {{ parameter.transform() }}
62+
if {{ parameter.python_name }} is not None:
63+
params["{{ parameter.name }}"] = str({{ parameter.transform() }})
5964
{% endif %}
6065
{% endfor %}
6166
{% endif %}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
{{ property.name }} = []
2-
for {{ property.name }}_item in d.get("{{ property.name }}", []):
3-
{{ property.name }}.append({{ property.reference.class_name }}({{ property.name }}_item))
1+
{{ property.python_name }} = []
2+
for {{ property.python_name }}_item in d.get("{{ property.name }}", []):
3+
{{ property.python_name }}.append({{ property.reference.class_name }}({{ property.python_name }}_item))

openapi_python_client/templates/model.pyi

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ from __future__ import annotations
22

33
from dataclasses import dataclass
44
from datetime import datetime
5-
from typing import Dict, List, Optional, cast
5+
from typing import Any, Dict, List, Optional, cast
66

77
{% for relative in schema.relative_imports %}
88
{{ relative }}
@@ -16,29 +16,29 @@ class {{ schema.reference.class_name }}:
1616
{{ property.to_string() }}
1717
{% endfor %}
1818

19-
def to_dict(self) -> Dict:
19+
def to_dict(self) -> Dict[str, Any]:
2020
return {
2121
{% for property in schema.required_properties %}
2222
"{{ property.name }}": self.{{ property.transform() }},
2323
{% endfor %}
2424
{% for property in schema.optional_properties %}
25-
"{{ property.name }}": self.{{ property.transform() }} if self.{{ property.name }} is not None else None,
26-
{% endfor %}
25+
"{{ property.name }}": self.{{ property.transform() }} if self.{{ property.python_name }} is not None else None,
26+
{% endfor %}
2727
}
2828

2929
@staticmethod
30-
def from_dict(d: Dict) -> {{ schema.reference.class_name }}:
30+
def from_dict(d: Dict[str, Any]) -> {{ schema.reference.class_name }}:
3131
{% for property in schema.required_properties + schema.optional_properties %}
3232

3333
{% if property.constructor_template %}
3434
{% include property.constructor_template %}
3535
{% else %}
36-
{{ property.name }} = {{ property.constructor_from_dict("d") }}
36+
{{ property.python_name }} = {{ property.constructor_from_dict("d") }}
3737
{% endif %}
3838

3939
{% endfor %}
4040
return {{ schema.reference.class_name }}(
4141
{% for property in schema.required_properties + schema.optional_properties %}
42-
{{ property.name }}={{ property.name }},
42+
{{ property.python_name }}={{ property.python_name }},
4343
{% endfor %}
4444
)
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% if property.required %}
2-
{{ property.name }} = {{ property.reference.class_name }}.from_dict(d["{{ property.name }}"])
2+
{{ property.python_name }} = {{ property.reference.class_name }}.from_dict(d["{{ property.name }}"])
33
{% else %}
4-
{{ property.name }} = None
5-
if ({{ property.name }}_data := d.get("{{ property.name }}")) is not None:
6-
{{ property.name }} = {{ property.reference.class_name }}.from_dict(cast(Dict, {{ property.name }}_data))
4+
{{ property.python_name }} = None
5+
if ({{ property.python_name }}_data := d.get("{{ property.name }}")) is not None:
6+
{{ property.python_name }} = {{ property.reference.class_name }}.from_dict(cast(Dict[str, Any], {{ property.python_name }}_data))
77
{% endif %}

0 commit comments

Comments
 (0)