openapi-python-client 0.5.2__tar.gz → 0.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/CHANGELOG.md +16 -0
  2. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/PKG-INFO +2 -2
  3. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/__init__.py +6 -3
  4. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/parser/errors.py +5 -1
  5. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/parser/openapi.py +6 -1
  6. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/parser/properties.py +110 -35
  7. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/async_endpoint_module.pyi +9 -3
  8. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/endpoint_macros.pyi +13 -0
  9. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/endpoint_module.pyi +9 -3
  10. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/property_templates/date_property.pyi +2 -2
  11. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/property_templates/datetime_property.pyi +2 -2
  12. openapi-python-client-0.5.3/openapi_python_client/templates/property_templates/dict_property.pyi +17 -0
  13. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/property_templates/union_property.pyi +3 -3
  14. openapi-python-client-0.5.3/openapi_python_client/utils.py +36 -0
  15. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/pyproject.toml +7 -2
  16. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/setup.py +2 -2
  17. openapi-python-client-0.5.2/openapi_python_client/utils.py +0 -13
  18. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/LICENSE +0 -0
  19. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/README.md +0 -0
  20. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/__main__.py +0 -0
  21. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/cli.py +0 -0
  22. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/config.py +0 -0
  23. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/parser/__init__.py +0 -0
  24. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/parser/reference.py +0 -0
  25. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/parser/responses.py +0 -0
  26. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/py.typed +0 -0
  27. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/LICENSE +0 -0
  28. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/README.md +0 -0
  29. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/__init__.py +0 -0
  30. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/components.py +0 -0
  31. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/contact.py +0 -0
  32. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/discriminator.py +0 -0
  33. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/encoding.py +0 -0
  34. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/example.py +0 -0
  35. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/external_documentation.py +0 -0
  36. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/header.py +0 -0
  37. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/info.py +0 -0
  38. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/license.py +0 -0
  39. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/link.py +0 -0
  40. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/media_type.py +0 -0
  41. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/oauth_flow.py +0 -0
  42. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/oauth_flows.py +0 -0
  43. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/open_api.py +0 -0
  44. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/operation.py +0 -0
  45. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/parameter.py +0 -0
  46. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/path_item.py +0 -0
  47. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/paths.py +0 -0
  48. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/reference.py +0 -0
  49. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/request_body.py +0 -0
  50. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/response.py +0 -0
  51. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/responses.py +0 -0
  52. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/schema.py +0 -0
  53. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/security_requirement.py +0 -0
  54. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/security_scheme.py +0 -0
  55. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/server.py +0 -0
  56. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/server_variable.py +0 -0
  57. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/tag.py +0 -0
  58. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/schema/xml.py +0 -0
  59. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/.gitignore +0 -0
  60. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/README.md +0 -0
  61. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/client.pyi +0 -0
  62. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/enum.pyi +0 -0
  63. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/errors.pyi +0 -0
  64. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/model.pyi +0 -0
  65. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/models_init.pyi +0 -0
  66. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/package_init.pyi +0 -0
  67. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/property_templates/enum_property.pyi +0 -0
  68. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/property_templates/file_property.pyi +0 -0
  69. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/property_templates/list_property.pyi +0 -0
  70. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/property_templates/ref_property.pyi +0 -0
  71. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/pyproject.toml +0 -0
  72. {openapi-python-client-0.5.2 → openapi-python-client-0.5.3}/openapi_python_client/templates/types.py +0 -0
@@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
 
8
+ ## 0.5.3 - 2020-08-13
9
+ ### Security
10
+ - All values that become file/directory names are sanitized to address path traversal vulnerabilities (CVE-2020-15141)
11
+ - All values that get placed into python files (everything from enum names, to endpoint descriptions, to default values) are validated and/or saniziatied to address arbitrary code execution vulnerabilities (CVE-2020-15142)
12
+
13
+ ### Changes
14
+ - Due to security concerns/implementation complexities, default values are temporarily unsupported for any `RefProperty` that doesn't refer to an enum.
15
+ - Defaults for properties must now be valid values for their respective type (e.g. "example string" is an invalid default for an `integer` type property, and the function for an endpoint using it would fail to generate and be skipped).
16
+
17
+ ### Additions
18
+ - Added support for header parameters (#117)
19
+
20
+ ### Fixes
21
+ - JSON bodies will now be assigned correctly in generated clients(#139 & #147). Thanks @pawamoy!
22
+
23
+
8
24
  ## 0.5.2 - 2020-08-06
9
25
  ### Additions
10
26
  - Added `project_name_override` and `package_name_override` config options to override the name of the generated project/package (#123)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openapi-python-client
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Generate modern Python clients from OpenAPI
5
5
  Home-page: https://github.com/triaxtec/openapi-python-client
6
6
  License: MIT
@@ -18,7 +18,7 @@ Classifier: Topic :: Software Development :: Code Generators
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Dist: black (>=19.10b0,<20.0)
20
20
  Requires-Dist: colorama (>=0.4.3,<0.5.0); sys_platform == "win32"
21
- Requires-Dist: httpx (>=0.13.0,<0.14.0)
21
+ Requires-Dist: httpx (>=0.13,<0.15)
22
22
  Requires-Dist: importlib_metadata (>=1.6.0,<2.0.0); python_version == "3.7"
23
23
  Requires-Dist: isort (>=5.0.5,<6.0.0)
24
24
  Requires-Dist: jinja2 (>=2.11.1,<3.0.0)
@@ -83,7 +83,7 @@ def _get_document(*, url: Optional[str], path: Optional[Path]) -> Union[Dict[str
83
83
 
84
84
 
85
85
  class Project:
86
- TEMPLATE_FILTERS = {"snakecase": utils.snake_case}
86
+ TEMPLATE_FILTERS = {"snakecase": utils.snake_case, "kebabcase": utils.kebab_case}
87
87
  project_name_override: Optional[str] = None
88
88
  package_name_override: Optional[str] = None
89
89
 
@@ -91,12 +91,14 @@ class Project:
91
91
  self.openapi: GeneratorData = openapi
92
92
  self.env: Environment = Environment(loader=PackageLoader(__package__), trim_blocks=True, lstrip_blocks=True)
93
93
 
94
- self.project_name: str = self.project_name_override or f"{openapi.title.replace(' ', '-').lower()}-client"
94
+ self.project_name: str = self.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client"
95
95
  self.project_dir: Path = Path.cwd() / self.project_name
96
96
 
97
97
  self.package_name: str = self.package_name_override or self.project_name.replace("-", "_")
98
98
  self.package_dir: Path = self.project_dir / self.package_name
99
- self.package_description: str = f"A client library for accessing {self.openapi.title}"
99
+ self.package_description: str = utils.remove_string_escapes(
100
+ f"A client library for accessing {self.openapi.title}"
101
+ )
100
102
  self.version: str = openapi.version
101
103
 
102
104
  self.env.filters.update(self.TEMPLATE_FILTERS)
@@ -231,6 +233,7 @@ class Project:
231
233
  endpoint_template = self.env.get_template("endpoint_module.pyi")
232
234
  async_endpoint_template = self.env.get_template("async_endpoint_module.pyi")
233
235
  for tag, collection in self.openapi.endpoint_collections_by_tag.items():
236
+ tag = utils.snake_case(tag)
234
237
  module_path = api_dir / f"{tag}.py"
235
238
  module_path.write_text(endpoint_template.render(collection=collection))
236
239
  async_module_path = async_api_dir / f"{tag}.py"
@@ -2,7 +2,7 @@ from dataclasses import dataclass
2
2
  from enum import Enum
3
3
  from typing import Optional
4
4
 
5
- __all__ = ["GeneratorError", "ParseError", "PropertyError"]
5
+ __all__ = ["GeneratorError", "ParseError", "PropertyError", "ValidationError"]
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
@@ -37,3 +37,7 @@ class PropertyError(ParseError):
37
37
  """ Error raised when there's a problem creating a Property """
38
38
 
39
39
  header = "Problem creating a Property: "
40
+
41
+
42
+ class ValidationError(Exception):
43
+ pass
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Set, Union
8
8
  from pydantic import ValidationError
9
9
 
10
10
  from .. import schema as oai
11
+ from .. import utils
11
12
  from .errors import GeneratorError, ParseError, PropertyError
12
13
  from .properties import EnumProperty, Property, property_from_data
13
14
  from .reference import Reference
@@ -19,6 +20,7 @@ class ParameterLocation(str, Enum):
19
20
 
20
21
  QUERY = "query"
21
22
  PATH = "path"
23
+ HEADER = "header"
22
24
 
23
25
 
24
26
  def import_string_from_reference(reference: Reference, prefix: str = "") -> str:
@@ -78,6 +80,7 @@ class Endpoint:
78
80
  relative_imports: Set[str] = field(default_factory=set)
79
81
  query_parameters: List[Property] = field(default_factory=list)
80
82
  path_parameters: List[Property] = field(default_factory=list)
83
+ header_parameters: List[Property] = field(default_factory=list)
81
84
  responses: List[Response] = field(default_factory=list)
82
85
  form_body_reference: Optional[Reference] = None
83
86
  json_body: Optional[Property] = None
@@ -164,6 +167,8 @@ class Endpoint:
164
167
  endpoint.query_parameters.append(prop)
165
168
  elif param.param_in == ParameterLocation.PATH:
166
169
  endpoint.path_parameters.append(prop)
170
+ elif param.param_in == ParameterLocation.HEADER:
171
+ endpoint.header_parameters.append(prop)
167
172
  else:
168
173
  return ParseError(data=param, detail="Parameter must be declared in path or query")
169
174
  return endpoint
@@ -178,7 +183,7 @@ class Endpoint:
178
183
  endpoint = Endpoint(
179
184
  path=path,
180
185
  method=method,
181
- description=data.description,
186
+ description=utils.remove_string_escapes(data.description) if data.description else "",
182
187
  name=data.operationId,
183
188
  requires_security=bool(data.security),
184
189
  tag=tag,
@@ -1,11 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import InitVar, dataclass, field
4
+ from datetime import date, datetime
4
5
  from typing import Any, ClassVar, Dict, Generic, List, Optional, Set, TypeVar, Union
5
6
 
6
7
  from .. import schema as oai
7
8
  from .. import utils
8
- from .errors import PropertyError
9
+ from .errors import PropertyError, ValidationError
9
10
  from .reference import Reference
10
11
 
11
12
 
@@ -19,6 +20,9 @@ class Property:
19
20
  templates/property_templates and must contain two macros: construct and transform. Construct will be used to
20
21
  build this property from JSON data (a response from an API). Transform will be used to convert this property
21
22
  to JSON data (when sending a request to the API).
23
+
24
+ Raises:
25
+ ValidationError: Raised when the default value fails to be converted to the expected type
22
26
  """
23
27
 
24
28
  name: str
@@ -32,10 +36,16 @@ class Property:
32
36
 
33
37
  def __post_init__(self) -> None:
34
38
  self.python_name = utils.snake_case(self.name)
39
+ if self.default is not None:
40
+ self.default = self._validate_default(default=self.default)
41
+
42
+ def _validate_default(self, default: Any) -> Any:
43
+ """ Check that the default value is valid for the property's type + perform any necessary sanitization """
44
+ raise ValidationError
35
45
 
36
- def get_type_string(self) -> str:
46
+ def get_type_string(self, no_optional: bool = False) -> str:
37
47
  """ Get a string representation of type that should be used when declaring this property """
38
- if self.required:
48
+ if self.required or no_optional:
39
49
  return self._type_string
40
50
  return f"Optional[{self._type_string}]"
41
51
 
@@ -74,10 +84,8 @@ class StringProperty(Property):
74
84
 
75
85
  _type_string: ClassVar[str] = "str"
76
86
 
77
- def __post_init__(self) -> None:
78
- super().__post_init__()
79
- if self.default is not None:
80
- self.default = f'"{self.default}"'
87
+ def _validate_default(self, default: Any) -> str:
88
+ return f'"{utils.remove_string_escapes(default)}"'
81
89
 
82
90
 
83
91
  @dataclass
@@ -86,7 +94,7 @@ class DateTimeProperty(Property):
86
94
  A property of type datetime.datetime
87
95
  """
88
96
 
89
- _type_string: ClassVar[str] = "datetime"
97
+ _type_string: ClassVar[str] = "datetime.datetime"
90
98
  template: ClassVar[str] = "datetime_property.pyi"
91
99
 
92
100
  def get_imports(self, *, prefix: str) -> Set[str]:
@@ -97,15 +105,23 @@ class DateTimeProperty(Property):
97
105
  prefix: A prefix to put before any relative (local) module names.
98
106
  """
99
107
  imports = super().get_imports(prefix=prefix)
100
- imports.update({"from datetime import datetime", "from typing import cast"})
108
+ imports.update({"import datetime", "from typing import cast"})
101
109
  return imports
102
110
 
111
+ def _validate_default(self, default: Any) -> str:
112
+ for format_string in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S%z"):
113
+ try:
114
+ return repr(datetime.strptime(default, format_string))
115
+ except (TypeError, ValueError):
116
+ continue
117
+ raise ValidationError
118
+
103
119
 
104
120
  @dataclass
105
121
  class DateProperty(Property):
106
122
  """ A property of type datetime.date """
107
123
 
108
- _type_string: ClassVar[str] = "date"
124
+ _type_string: ClassVar[str] = "datetime.date"
109
125
  template: ClassVar[str] = "date_property.pyi"
110
126
 
111
127
  def get_imports(self, *, prefix: str) -> Set[str]:
@@ -116,9 +132,15 @@ class DateProperty(Property):
116
132
  prefix: A prefix to put before any relative (local) module names.
117
133
  """
118
134
  imports = super().get_imports(prefix=prefix)
119
- imports.update({"from datetime import date", "from typing import cast"})
135
+ imports.update({"import datetime", "from typing import cast"})
120
136
  return imports
121
137
 
138
+ def _validate_default(self, default: Any) -> str:
139
+ try:
140
+ return repr(date.fromisoformat(default))
141
+ except (TypeError, ValueError) as e:
142
+ raise ValidationError() from e
143
+
122
144
 
123
145
  @dataclass
124
146
  class FileProperty(Property):
@@ -146,6 +168,12 @@ class FloatProperty(Property):
146
168
  default: Optional[float] = None
147
169
  _type_string: ClassVar[str] = "float"
148
170
 
171
+ def _validate_default(self, default: Any) -> float:
172
+ try:
173
+ return float(default)
174
+ except (TypeError, ValueError) as e:
175
+ raise ValidationError() from e
176
+
149
177
 
150
178
  @dataclass
151
179
  class IntProperty(Property):
@@ -154,6 +182,12 @@ class IntProperty(Property):
154
182
  default: Optional[int] = None
155
183
  _type_string: ClassVar[str] = "int"
156
184
 
185
+ def _validate_default(self, default: Any) -> int:
186
+ try:
187
+ return int(default)
188
+ except (TypeError, ValueError) as e:
189
+ raise ValidationError() from e
190
+
157
191
 
158
192
  @dataclass
159
193
  class BooleanProperty(Property):
@@ -161,6 +195,10 @@ class BooleanProperty(Property):
161
195
 
162
196
  _type_string: ClassVar[str] = "bool"
163
197
 
198
+ def _validate_default(self, default: Any) -> bool:
199
+ # no try/except needed as anything that comes from the initial load from json/yaml will be boolable
200
+ return bool(default)
201
+
164
202
 
165
203
  InnerProp = TypeVar("InnerProp", bound=Property)
166
204
 
@@ -172,14 +210,9 @@ class ListProperty(Property, Generic[InnerProp]):
172
210
  inner_property: InnerProp
173
211
  template: ClassVar[str] = "list_property.pyi"
174
212
 
175
- def __post_init__(self) -> None:
176
- super().__post_init__()
177
- if self.default is not None:
178
- self.default = f"field(default_factory=lambda: cast({self.get_type_string()}, {self.default}))"
179
-
180
- def get_type_string(self) -> str:
213
+ def get_type_string(self, no_optional: bool = False) -> str:
181
214
  """ Get a string representation of type that should be used when declaring this property """
182
- if self.required:
215
+ if self.required or no_optional:
183
216
  return f"List[{self.inner_property.get_type_string()}]"
184
217
  return f"Optional[List[{self.inner_property.get_type_string()}]]"
185
218
 
@@ -198,6 +231,16 @@ class ListProperty(Property, Generic[InnerProp]):
198
231
  imports.add("from typing import cast")
199
232
  return imports
200
233
 
234
+ def _validate_default(self, default: Any) -> str:
235
+ if not isinstance(default, list):
236
+ raise ValidationError()
237
+
238
+ default = list(map(self.inner_property._validate_default, default))
239
+ if isinstance(self.inner_property, RefProperty): # Fix enums to use the actual value
240
+ default = str(default).replace("'", "")
241
+
242
+ return f"field(default_factory=lambda: cast({self.get_type_string()}, {default}))"
243
+
201
244
 
202
245
  @dataclass
203
246
  class UnionProperty(Property):
@@ -206,11 +249,11 @@ class UnionProperty(Property):
206
249
  inner_properties: List[Property]
207
250
  template: ClassVar[str] = "union_property.pyi"
208
251
 
209
- def get_type_string(self) -> str:
252
+ def get_type_string(self, no_optional: bool = False) -> str:
210
253
  """ Get a string representation of type that should be used when declaring this property """
211
254
  inner_types = [p.get_type_string() for p in self.inner_properties]
212
255
  inner_prop_string = ", ".join(inner_types)
213
- if self.required:
256
+ if self.required or no_optional:
214
257
  return f"Union[{inner_prop_string}]"
215
258
  return f"Optional[Union[{inner_prop_string}]]"
216
259
 
@@ -227,6 +270,15 @@ class UnionProperty(Property):
227
270
  imports.add("from typing import Union")
228
271
  return imports
229
272
 
273
+ def _validate_default(self, default: Any) -> Any:
274
+ for property in self.inner_properties:
275
+ try:
276
+ val = property._validate_default(default)
277
+ return val
278
+ except ValidationError:
279
+ continue
280
+ raise ValidationError()
281
+
230
282
 
231
283
  _existing_enums: Dict[str, EnumProperty] = {}
232
284
 
@@ -242,7 +294,6 @@ class EnumProperty(Property):
242
294
  template: ClassVar[str] = "enum_property.pyi"
243
295
 
244
296
  def __post_init__(self, title: str) -> None: # type: ignore
245
- super().__post_init__()
246
297
  reference = Reference.from_ref(title)
247
298
  dedup_counter = 0
248
299
  while reference.class_name in _existing_enums:
@@ -253,9 +304,7 @@ class EnumProperty(Property):
253
304
  reference = Reference.from_ref(f"{reference.class_name}{dedup_counter}")
254
305
 
255
306
  self.reference = reference
256
- inverse_values = {v: k for k, v in self.values.items()}
257
- if self.default is not None:
258
- self.default = f"{self.reference.class_name}.{inverse_values[self.default]}"
307
+ super().__post_init__()
259
308
  _existing_enums[self.reference.class_name] = self
260
309
 
261
310
  @staticmethod
@@ -268,10 +317,10 @@ class EnumProperty(Property):
268
317
  """ Get all the EnumProperties that have been registered keyed by class name """
269
318
  return _existing_enums.get(name)
270
319
 
271
- def get_type_string(self) -> str:
320
+ def get_type_string(self, no_optional: bool = False) -> str:
272
321
  """ Get a string representation of type that should be used when declaring this property """
273
322
 
274
- if self.required:
323
+ if self.required or no_optional:
275
324
  return self.reference.class_name
276
325
  return f"Optional[{self.reference.class_name}]"
277
326
 
@@ -298,10 +347,18 @@ class EnumProperty(Property):
298
347
  key = f"VALUE_{i}"
299
348
  if key in output:
300
349
  raise ValueError(f"Duplicate key {key} in Enum")
301
- output[key] = value
350
+ sanitized_key = utils.fix_keywords(utils.sanitize(key))
351
+ output[sanitized_key] = utils.remove_string_escapes(value)
302
352
 
303
353
  return output
304
354
 
355
+ def _validate_default(self, default: Any) -> str:
356
+ inverse_values = {v: k for k, v in self.values.items()}
357
+ try:
358
+ return f"{self.reference.class_name}.{inverse_values[default]}"
359
+ except KeyError as e:
360
+ raise ValidationError() from e
361
+
305
362
 
306
363
  @dataclass
307
364
  class RefProperty(Property):
@@ -316,9 +373,9 @@ class RefProperty(Property):
316
373
  return "enum_property.pyi"
317
374
  return "ref_property.pyi"
318
375
 
319
- def get_type_string(self) -> str:
376
+ def get_type_string(self, no_optional: bool = False) -> str:
320
377
  """ Get a string representation of type that should be used when declaring this property """
321
- if self.required:
378
+ if self.required or no_optional:
322
379
  return self.reference.class_name
323
380
  return f"Optional[{self.reference.class_name}]"
324
381
 
@@ -339,17 +396,20 @@ class RefProperty(Property):
339
396
  )
340
397
  return imports
341
398
 
399
+ def _validate_default(self, default: Any) -> Any:
400
+ enum = EnumProperty.get_enum(self.reference.class_name)
401
+ if enum:
402
+ return enum._validate_default(default)
403
+ else:
404
+ raise ValidationError
405
+
342
406
 
343
407
  @dataclass
344
408
  class DictProperty(Property):
345
409
  """ Property that is a general Dict """
346
410
 
347
411
  _type_string: ClassVar[str] = "Dict[Any, Any]"
348
-
349
- def __post_init__(self) -> None:
350
- super().__post_init__()
351
- if self.default is not None:
352
- self.default = f"field(default_factory=lambda: cast({self.get_type_string()}, {self.default}))"
412
+ template: ClassVar[str] = "dict_property.pyi"
353
413
 
354
414
  def get_imports(self, *, prefix: str) -> Set[str]:
355
415
  """
@@ -365,6 +425,11 @@ class DictProperty(Property):
365
425
  imports.add("from typing import cast")
366
426
  return imports
367
427
 
428
+ def _validate_default(self, default: Any) -> str:
429
+ if isinstance(default, dict):
430
+ return repr(default)
431
+ raise ValidationError
432
+
368
433
 
369
434
  def _string_based_property(
370
435
  name: str, required: bool, data: oai.Schema
@@ -381,10 +446,11 @@ def _string_based_property(
381
446
  return StringProperty(name=name, default=data.default, required=required, pattern=data.pattern)
382
447
 
383
448
 
384
- def property_from_data(
449
+ def _property_from_data(
385
450
  name: str, required: bool, data: Union[oai.Reference, oai.Schema]
386
451
  ) -> Union[Property, PropertyError]:
387
452
  """ Generate a Property from the OpenAPI dictionary representation of it """
453
+ name = utils.remove_string_escapes(name)
388
454
  if isinstance(data, oai.Reference):
389
455
  return RefProperty(name=name, required=required, reference=Reference.from_ref(data.ref), default=None)
390
456
  if data.enum:
@@ -423,3 +489,12 @@ def property_from_data(
423
489
  elif data.type == "object":
424
490
  return DictProperty(name=name, required=required, default=data.default)
425
491
  return PropertyError(data=data, detail=f"unknown type {data.type}")
492
+
493
+
494
+ def property_from_data(
495
+ name: str, required: bool, data: Union[oai.Reference, oai.Schema]
496
+ ) -> Union[Property, PropertyError]:
497
+ try:
498
+ return _property_from_data(name=name, required=required, data=data)
499
+ except ValidationError:
500
+ return PropertyError(detail="Failed to validate default value", data=data)
@@ -11,7 +11,7 @@ from ..errors import ApiResponseError
11
11
  {% endfor %}
12
12
  {% for endpoint in collection.endpoints %}
13
13
 
14
- {% from "endpoint_macros.pyi" import query_params, json_body, return_type %}
14
+ {% from "endpoint_macros.pyi" import header_params, query_params, json_body, return_type %}
15
15
 
16
16
  async def {{ endpoint.name | snakecase }}(
17
17
  *,
@@ -41,6 +41,9 @@ async def {{ endpoint.name | snakecase }}(
41
41
  {% for parameter in endpoint.query_parameters %}
42
42
  {{ parameter.to_string() }},
43
43
  {% endfor %}
44
+ {% for parameter in endpoint.header_parameters %}
45
+ {{ parameter.to_string() }},
46
+ {% endfor %}
44
47
  {{ return_type(endpoint) }}
45
48
  """ {{ endpoint.description }} """
46
49
  url = "{}{{ endpoint.path }}".format(
@@ -50,13 +53,16 @@ async def {{ endpoint.name | snakecase }}(
50
53
  {% endfor %}
51
54
  )
52
55
 
56
+ headers: Dict[str, Any] = client.get_headers()
57
+ {{ header_params(endpoint) | indent(4) }}
58
+
53
59
  {{ query_params(endpoint) | indent(4) }}
54
60
  {{ json_body(endpoint) | indent(4) }}
55
61
 
56
62
  async with httpx.AsyncClient() as _client:
57
63
  response = await _client.{{ endpoint.method }}(
58
64
  url=url,
59
- headers=client.get_headers(),
65
+ headers=headers,
60
66
  {% if endpoint.form_body_reference %}
61
67
  data=asdict(form_data),
62
68
  {% endif %}
@@ -64,7 +70,7 @@ async def {{ endpoint.name | snakecase }}(
64
70
  files=multipart_data.to_dict(),
65
71
  {% endif %}
66
72
  {% if endpoint.json_body %}
67
- json={{ endpoint.json_body.python_name }},
73
+ json={{ "json_" + endpoint.json_body.python_name }},
68
74
  {% endif %}
69
75
  {% if endpoint.query_parameters %}
70
76
  params=params,
@@ -1,3 +1,16 @@
1
+ {% macro header_params(endpoint) %}
2
+ {% if endpoint.header_parameters %}
3
+ {% for parameter in endpoint.header_parameters %}
4
+ {% if parameter.required %}
5
+ headers["{{ parameter.python_name | kebabcase}}"] = {{ parameter.python_name }}
6
+ {% else %}
7
+ if {{ parameter.python_name }} is not None:
8
+ headers["{{ parameter.python_name | kebabcase}}"] = {{ parameter.python_name }}
9
+ {% endif %}
10
+ {% endfor %}
11
+ {% endif %}
12
+ {% endmacro %}
13
+
1
14
  {% macro query_params(endpoint) %}
2
15
  {% if endpoint.query_parameters %}
3
16
  {% for property in endpoint.query_parameters %}
@@ -11,7 +11,7 @@ from ..errors import ApiResponseError
11
11
  {% endfor %}
12
12
  {% for endpoint in collection.endpoints %}
13
13
 
14
- {% from "endpoint_macros.pyi" import query_params, json_body, return_type %}
14
+ {% from "endpoint_macros.pyi" import header_params, query_params, json_body, return_type %}
15
15
 
16
16
  def {{ endpoint.name | snakecase }}(
17
17
  *,
@@ -41,6 +41,9 @@ def {{ endpoint.name | snakecase }}(
41
41
  {% for parameter in endpoint.query_parameters %}
42
42
  {{ parameter.to_string() }},
43
43
  {% endfor %}
44
+ {% for parameter in endpoint.header_parameters %}
45
+ {{ parameter.to_string() }},
46
+ {% endfor %}
44
47
  {{ return_type(endpoint) }}
45
48
  """ {{ endpoint.description }} """
46
49
  url = "{}{{ endpoint.path }}".format(
@@ -50,6 +53,9 @@ def {{ endpoint.name | snakecase }}(
50
53
  {%- endfor -%}
51
54
  )
52
55
 
56
+ headers: Dict[str, Any] = client.get_headers()
57
+ {{ header_params(endpoint) | indent(4) }}
58
+
53
59
  {{ query_params(endpoint) | indent(4) }}
54
60
 
55
61
  {{ json_body(endpoint) | indent(4) }}
@@ -57,7 +63,7 @@ def {{ endpoint.name | snakecase }}(
57
63
 
58
64
  response = httpx.{{ endpoint.method }}(
59
65
  url=url,
60
- headers=client.get_headers(),
66
+ headers=headers,
61
67
  {% if endpoint.form_body_reference %}
62
68
  data=asdict(form_data),
63
69
  {% endif %}
@@ -65,7 +71,7 @@ def {{ endpoint.name | snakecase }}(
65
71
  files=multipart_data.to_dict(),
66
72
  {% endif %}
67
73
  {% if endpoint.json_body %}
68
- json={{ endpoint.json_body.python_name }},
74
+ json={{ "json_" + endpoint.json_body.python_name }},
69
75
  {% endif %}
70
76
  {% if endpoint.query_parameters %}
71
77
  params=params,
@@ -1,10 +1,10 @@
1
1
  {% macro construct(property, source) %}
2
2
  {% if property.required %}
3
- {{ property.python_name }} = date.fromisoformat({{ source }})
3
+ {{ property.python_name }} = datetime.date.fromisoformat({{ source }})
4
4
  {% else %}
5
5
  {{ property.python_name }} = None
6
6
  if {{ source }} is not None:
7
- {{ property.python_name }} = date.fromisoformat(cast(str, {{ source }}))
7
+ {{ property.python_name }} = datetime.date.fromisoformat(cast(str, {{ source }}))
8
8
  {% endif %}
9
9
  {% endmacro %}
10
10
 
@@ -1,10 +1,10 @@
1
1
  {% macro construct(property, source) %}
2
2
  {% if property.required %}
3
- {{ property.python_name }} = datetime.fromisoformat({{ source }})
3
+ {{ property.python_name }} = datetime.datetime.fromisoformat({{ source }})
4
4
  {% else %}
5
5
  {{ property.python_name }} = None
6
6
  if {{ source }} is not None:
7
- {{ property.python_name }} = datetime.fromisoformat(cast(str, {{ source }}))
7
+ {{ property.python_name }} = datetime.datetime.fromisoformat(cast(str, {{ source }}))
8
8
  {% endif %}
9
9
  {% endmacro %}
10
10
 
@@ -0,0 +1,17 @@
1
+ {% macro construct(property, source) %}
2
+ {% if property.required %}
3
+ {{ property.python_name }} = {{ source }}
4
+ {% else %}
5
+ {{ property.python_name }} = None
6
+ if {{ source }} is not None:
7
+ {{ property.python_name }} = {{ source }}
8
+ {% endif %}
9
+ {% endmacro %}
10
+
11
+ {% macro transform(property, source, destination) %}
12
+ {% if property.required %}
13
+ {{ destination }} = {{ source }}
14
+ {% else %}
15
+ {{ destination }} = {{ source }} if {{ source }} else None
16
+ {% endif %}
17
+ {% endmacro %}
@@ -24,13 +24,13 @@ def _parse_{{ property.python_name }}(data: Dict[str, Any]) -> {{ property.get_t
24
24
  {% macro transform(property, source, destination) %}
25
25
  {% if not property.required %}
26
26
  if {{ source }} is None:
27
- {{ destination }} = None
27
+ {{ destination }}: {{ property.get_type_string() }} = None
28
28
  {% endif %}
29
29
  {% for inner_property in property.inner_properties %}
30
30
  {% if loop.first and property.required %}{# No if None statement before this #}
31
- if isinstance({{ source }}, {{ inner_property.get_type_string() }}):
31
+ if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}):
32
32
  {% elif not loop.last %}
33
- elif isinstance({{ source }}, {{ inner_property.get_type_string() }}):
33
+ elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}):
34
34
  {% else %}
35
35
  else:
36
36
  {% endif %}
@@ -0,0 +1,36 @@
1
+ import re
2
+ from keyword import iskeyword
3
+
4
+ import stringcase
5
+
6
+
7
+ def sanitize(value: str) -> str:
8
+ return re.sub(r"[^\w _\-]+", "", value)
9
+
10
+
11
+ def fix_keywords(value: str) -> str:
12
+ if iskeyword(value):
13
+ return f"{value}_"
14
+ return value
15
+
16
+
17
+ def group_title(value: str) -> str:
18
+ value = re.sub(r"([A-Z]{2,})([A-Z][a-z]|[ \-_]|$)", lambda m: m.group(1).title() + m.group(2), value.strip())
19
+ value = re.sub(r"(^|[ _-])([A-Z])", lambda m: m.group(1) + m.group(2).lower(), value)
20
+ return value
21
+
22
+
23
+ def snake_case(value: str) -> str:
24
+ return fix_keywords(stringcase.snakecase(group_title(sanitize(value))))
25
+
26
+
27
+ def pascal_case(value: str) -> str:
28
+ return fix_keywords(stringcase.pascalcase(sanitize(value)))
29
+
30
+
31
+ def kebab_case(value: str) -> str:
32
+ return fix_keywords(stringcase.spinalcase(group_title(sanitize(value))))
33
+
34
+
35
+ def remove_string_escapes(value: str) -> str:
36
+ return value.replace('"', r"\"")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openapi-python-client"
3
- version = "0.5.2"
3
+ version = "0.5.3"
4
4
  description = "Generate modern Python clients from OpenAPI"
5
5
  repository = "https://github.com/triaxtec/openapi-python-client"
6
6
  license = "MIT"
@@ -25,7 +25,7 @@ stringcase = "^1.2.0"
25
25
  typer = ">=0.1,<0.4"
26
26
  colorama = {version = "^0.4.3", markers = "sys_platform == 'win32'"}
27
27
  shellingham = "^1.3.2"
28
- httpx = "^0.13.0"
28
+ httpx = ">=0.13,<0.15"
29
29
  black = "^19.10b0"
30
30
  isort = "^5.0.5"
31
31
  pyyaml = "^5.3.1"
@@ -58,6 +58,11 @@ isort .\
58
58
  openapi = "python -m end_to_end_tests.fastapi_app"
59
59
  gm = "python -m end_to_end_tests.regen_golden_master"
60
60
  e2e = "pytest openapi_python_client end_to_end_tests"
61
+ oge = """
62
+ task openapi\
63
+ && task gm\
64
+ && task e2e\
65
+ """
61
66
 
62
67
  [tool.black]
63
68
  line-length = 120
@@ -12,7 +12,7 @@ package_data = \
12
12
 
13
13
  install_requires = \
14
14
  ['black>=19.10b0,<20.0',
15
- 'httpx>=0.13.0,<0.14.0',
15
+ 'httpx>=0.13,<0.15',
16
16
  'isort>=5.0.5,<6.0.0',
17
17
  'jinja2>=2.11.1,<3.0.0',
18
18
  'pydantic>=1.6.1,<2.0.0',
@@ -30,7 +30,7 @@ entry_points = \
30
30
 
31
31
  setup_kwargs = {
32
32
  'name': 'openapi-python-client',
33
- 'version': '0.5.2',
33
+ 'version': '0.5.3',
34
34
  'description': 'Generate modern Python clients from OpenAPI',
35
35
  'long_description': '[![triaxtec](https://circleci.com/gh/triaxtec/openapi-python-client.svg?style=svg)](https://circleci.com/gh/triaxtec/openapi-python-client)\n[![codecov](https://codecov.io/gh/triaxtec/openapi-python-client/branch/master/graph/badge.svg)](https://codecov.io/gh/triaxtec/openapi-python-client)\n[![PyPI version shields.io](https://img.shields.io/pypi/v/openapi-python-client.svg)](https://pypi.python.org/pypi/openapi-python-client/)\n[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/)\n[![Generic badge](https://img.shields.io/badge/type_checked-mypy-informational.svg)](https://mypy.readthedocs.io/en/stable/introduction.html)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)\n\n\n# openapi-python-client\nGenerate modern Python clients from OpenAPI\n\n**This project is still in development and does not support all OpenAPI features**\n\n## Why This?\nThe Python clients generated by openapi-generator support Python 2 and therefore come with a lot of baggage. This tool \naims to generate clients which:\n1. Use all the latest and greatest Python features like type annotations and dataclasses\n1. Don\'t carry around a bunch of compatibility code for older version of Python (e.g. the `six` package)\n1. Have better documentation and more obvious usage instructions\n\nAdditionally, because this generator is written in Python, it should be more accessible to contribution by the people \nusing it (Python developers).\n\n## Installation\nI recommend you install with [pipx](https://pipxproject.github.io/pipx/) so you don\'t conflict with any other packages \nyou might have: `pipx install openapi-python-client`.\n\nBetter yet, use `pipx run openapi-python-client <normal params / options>` to always use the latest version of the generator.\n\nYou can install with normal pip if you want to though: `pip install openapi-python-client`\n\nThen, if you want tab completion: `openapi-python-client --install-completion`\n\n## Usage\n### Create a new client\n`openapi-python-client generate --url https://my.api.com/openapi.json`\n\nThis will generate a new client library named based on the title in your OpenAPI spec. For example, if the title \nof your API is "My API", the expected output will be "my-api-client". If a folder already exists by that name, you\'ll \nget an error.\n\n### Update an existing client\n`openapi-python-client update --url https://my.api.com/openapi.json`\n\n> For more usage details run `openapi-python-client --help` or read [usage](usage.md)\n\n## What You Get\n1. A `pyproject.toml` file with some basic metadata intended to be used with [Poetry].\n1. A `README.md` you\'ll most definitely need to update with your project\'s details\n1. A Python module named just like the auto-generated project name (e.g. "my_api_client") which contains:\n 1. A `client` module which will have both a `Client` class and an `AuthenticatedClient` class. You\'ll need these \n for calling the functions in the `api` module.\n 1. An `api` module which will contain one module for each tag in your OpenAPI spec, as well as a `default` module \n for endpoints without a tag. Each of these modules in turn contains one function for calling each endpoint.\n 1. A `models` module which has all the classes defined by the various schemas in your OpenAPI spec\n \nFor a full example you can look at the `test_end_to_end` directory which has a declared [FastAPI](https://fastapi.tiangolo.com/) \nserver and the resulting openapi.json file in the "fastapi" directory. "golden-master" is the generated client from that \nOpenAPI document.\n \n## OpenAPI features supported\n1. All HTTP Methods\n1. JSON and form bodies, path and query parameters\n1. File uploads with multipart/form-data bodies\n1. float, string, int, date, datetime, string enums, and custom schemas or lists containing any of those\n1. html/text or application/json responses containing any of the previous types\n1. Bearer token security\n\n## Configuration\nYou can pass a YAML (or JSON) file to openapi-python-client with the `--config` option in order to change some behavior.\nThe following parameters are supported:\n\n### class_overrides\nUsed to change the name of generated model classes. This param should be a mapping of existing class name \n(usually a key in the "schemas" section of your OpenAPI document) to class_name and module_name. As an example, if the \nname of the a model in OpenAPI (and therefore the generated class name) was something like "_PrivateInternalLongName" \nand you want the generated client\'s model to be called "ShortName" in a module called "short_name" you could do this:\n\nExample:\n```yaml\nclass_overrides:\n _PrivateInternalLongName:\n class_name: ShortName\n module_name: short_name\n```\n\nThe easiest way to find what needs to be overridden is probably to generate your client and go look at everything in the\n models folder.\n \n ### project_name_override and package_name_override\nUsed to change the name of generated client library project/package. If the project name is changed but an override for the package name\nisn\'t provided, the package name will be converted from the project name using the standard convention (replacing `-`\'s with `_`\'s).\n\nExample:\n```yaml\nproject_name_override: my-special-project-name\npackage_name_override: my_extra_special_package_name\n```\n\n\n[CHANGELOG.md]: CHANGELOG.md\n[Poetry]: https://python-poetry.org/\n',
36
36
  'author': 'Dylan Anthony',
@@ -1,13 +0,0 @@
1
- import re
2
-
3
- import stringcase
4
-
5
-
6
- def snake_case(value: str) -> str:
7
- value = re.sub(r"([A-Z]{2,})([A-Z][a-z]|[ -_]|$)", lambda m: m.group(1).title() + m.group(2), value.strip())
8
- value = re.sub(r"(^|[ _-])([A-Z])", lambda m: m.group(1) + m.group(2).lower(), value)
9
- return stringcase.snakecase(value)
10
-
11
-
12
- def pascal_case(value: str) -> str:
13
- return stringcase.pascalcase(value)