openapi-python-client 0.17.3__tar.gz → 0.18.0__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 (103) hide show
  1. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/PKG-INFO +3 -3
  2. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/openapi.py +83 -66
  3. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/model_property.py +23 -2
  4. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/protocol.py +6 -2
  5. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/endpoint_macros.py.jinja +11 -11
  6. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/endpoint_module.py.jinja +1 -1
  7. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/pyproject.toml.jinja +2 -2
  8. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/pyproject_ruff.toml.jinja +3 -1
  9. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/setup.py.jinja +1 -1
  10. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/utils.py +5 -2
  11. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/pyproject.toml +7 -5
  12. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/.gitignore +0 -0
  13. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/LICENSE +0 -0
  14. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/README.md +0 -0
  15. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/__init__.py +0 -0
  16. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/__main__.py +0 -0
  17. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/cli.py +0 -0
  18. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/config.py +0 -0
  19. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/__init__.py +0 -0
  20. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/bodies.py +0 -0
  21. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/errors.py +0 -0
  22. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/__init__.py +0 -0
  23. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/any.py +0 -0
  24. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/boolean.py +0 -0
  25. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/const.py +0 -0
  26. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/date.py +0 -0
  27. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/datetime.py +0 -0
  28. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/enum_property.py +0 -0
  29. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/file.py +0 -0
  30. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/float.py +0 -0
  31. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/int.py +0 -0
  32. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/list_property.py +0 -0
  33. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/none.py +0 -0
  34. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/property.py +0 -0
  35. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/schemas.py +0 -0
  36. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/string.py +0 -0
  37. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/properties/union.py +0 -0
  38. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/parser/responses.py +0 -0
  39. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/py.typed +0 -0
  40. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/3.0.3.md +0 -0
  41. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/3.1.0.md +0 -0
  42. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/__init__.py +0 -0
  43. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/data_type.py +0 -0
  44. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/LICENSE +0 -0
  45. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/README.md +0 -0
  46. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/__init__.py +0 -0
  47. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/callback.py +0 -0
  48. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/components.py +0 -0
  49. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/contact.py +0 -0
  50. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +0 -0
  51. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/encoding.py +0 -0
  52. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/example.py +0 -0
  53. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +0 -0
  54. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/header.py +0 -0
  55. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/info.py +0 -0
  56. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/license.py +0 -0
  57. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/link.py +0 -0
  58. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/media_type.py +0 -0
  59. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +0 -0
  60. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py +0 -0
  61. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/open_api.py +0 -0
  62. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/operation.py +0 -0
  63. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/parameter.py +0 -0
  64. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/path_item.py +0 -0
  65. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/paths.py +0 -0
  66. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/reference.py +0 -0
  67. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/request_body.py +0 -0
  68. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/response.py +0 -0
  69. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/responses.py +0 -0
  70. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/schema.py +0 -0
  71. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +0 -0
  72. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +0 -0
  73. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/server.py +0 -0
  74. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +0 -0
  75. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/tag.py +0 -0
  76. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/openapi_schema_pydantic/xml.py +0 -0
  77. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/schema/parameter_location.py +0 -0
  78. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/.gitignore.jinja +0 -0
  79. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/README.md.jinja +0 -0
  80. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/api_init.py.jinja +0 -0
  81. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/client.py.jinja +0 -0
  82. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/endpoint_init.py.jinja +0 -0
  83. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/errors.py.jinja +0 -0
  84. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/helpers.jinja +0 -0
  85. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/int_enum.py.jinja +0 -0
  86. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/model.py.jinja +0 -0
  87. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/models_init.py.jinja +0 -0
  88. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/package_init.py.jinja +0 -0
  89. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/any_property.py.jinja +0 -0
  90. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/boolean_property.py.jinja +0 -0
  91. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/date_property.py.jinja +0 -0
  92. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/datetime_property.py.jinja +0 -0
  93. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/enum_property.py.jinja +0 -0
  94. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/file_property.py.jinja +0 -0
  95. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/float_property.py.jinja +0 -0
  96. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/helpers.jinja +0 -0
  97. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/int_property.py.jinja +0 -0
  98. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/list_property.py.jinja +0 -0
  99. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/model_property.py.jinja +0 -0
  100. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/property_macros.py.jinja +0 -0
  101. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/property_templates/union_property.py.jinja +0 -0
  102. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/str_enum.py.jinja +0 -0
  103. {openapi_python_client-0.17.3 → openapi_python_client-0.18.0}/openapi_python_client/templates/types.py.jinja +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openapi-python-client
3
- Version: 0.17.3
3
+ Version: 0.18.0
4
4
  Summary: Generate modern Python clients from OpenAPI
5
5
  Project-URL: repository, https://github.com/openapi-generators/openapi-python-client
6
6
  Author-email: Dylan Anthony <contact@dylananthony.com>
@@ -21,12 +21,12 @@ Classifier: Typing :: Typed
21
21
  Requires-Python: <4.0,>=3.8
22
22
  Requires-Dist: attrs>=21.3.0
23
23
  Requires-Dist: colorama>=0.4.3; sys_platform == 'win32'
24
- Requires-Dist: httpx<0.27.0,>=0.20.0
24
+ Requires-Dist: httpx<0.28.0,>=0.20.0
25
25
  Requires-Dist: jinja2<4.0.0,>=3.0.0
26
26
  Requires-Dist: pydantic<3.0.0,>=2.1.1
27
27
  Requires-Dist: python-dateutil<3.0.0,>=2.8.1
28
28
  Requires-Dist: pyyaml<7.0,>=6.0
29
- Requires-Dist: ruff<1.0.0,>=0.1.2
29
+ Requires-Dist: ruff<0.3,>=0.2
30
30
  Requires-Dist: shellingham<2.0.0,>=1.3.2
31
31
  Requires-Dist: typer<0.10,>0.6
32
32
  Requires-Dist: typing-extensions<5.0.0,>=4.8.0
@@ -1,5 +1,4 @@
1
1
  import re
2
- from collections import OrderedDict
3
2
  from copy import deepcopy
4
3
  from dataclasses import dataclass, field
5
4
  from http import HTTPStatus
@@ -14,7 +13,6 @@ from ..utils import PythonIdentifier
14
13
  from .bodies import Body, body_from_data
15
14
  from .errors import GeneratorError, ParseError, PropertyError
16
15
  from .properties import (
17
- AnyProperty,
18
16
  Class,
19
17
  EnumProperty,
20
18
  ModelProperty,
@@ -134,14 +132,13 @@ class Endpoint:
134
132
  tag: str
135
133
  summary: Optional[str] = ""
136
134
  relative_imports: Set[str] = field(default_factory=set)
137
- query_parameters: Dict[str, Property] = field(default_factory=dict)
138
- path_parameters: "OrderedDict[str, Property]" = field(default_factory=OrderedDict)
139
- header_parameters: Dict[str, Property] = field(default_factory=dict)
140
- cookie_parameters: Dict[str, Property] = field(default_factory=dict)
135
+ query_parameters: List[Property] = field(default_factory=list)
136
+ path_parameters: List[Property] = field(default_factory=list)
137
+ header_parameters: List[Property] = field(default_factory=list)
138
+ cookie_parameters: List[Property] = field(default_factory=list)
141
139
  responses: List[Response] = field(default_factory=list)
142
140
  bodies: List[Body] = field(default_factory=list)
143
141
  errors: List[ParseError] = field(default_factory=list)
144
- used_python_identifiers: Set[PythonIdentifier] = field(default_factory=set)
145
142
 
146
143
  @staticmethod
147
144
  def _add_responses(
@@ -191,7 +188,7 @@ class Endpoint:
191
188
  return endpoint, schemas
192
189
 
193
190
  @staticmethod
194
- def add_parameters( # noqa: PLR0911
191
+ def add_parameters(
195
192
  *,
196
193
  endpoint: "Endpoint",
197
194
  data: Union[oai.Operation, oai.PathItem],
@@ -228,22 +225,11 @@ class Endpoint:
228
225
  endpoint = deepcopy(endpoint)
229
226
 
230
227
  unique_parameters: Set[Tuple[str, oai.ParameterLocation]] = set()
231
- parameters_by_location: Dict[str, Dict[str, Property]] = {
228
+ parameters_by_location: Dict[str, List[Property]] = {
232
229
  oai.ParameterLocation.QUERY: endpoint.query_parameters,
233
230
  oai.ParameterLocation.PATH: endpoint.path_parameters,
234
231
  oai.ParameterLocation.HEADER: endpoint.header_parameters,
235
232
  oai.ParameterLocation.COOKIE: endpoint.cookie_parameters,
236
- "RESERVED": { # These can't be param names because codegen needs them as vars, the properties don't matter
237
- "client": AnyProperty(
238
- "client",
239
- True,
240
- None,
241
- PythonIdentifier("client", ""),
242
- None,
243
- None,
244
- ),
245
- "url": AnyProperty("url", True, None, PythonIdentifier("url", ""), None, None),
246
- },
247
233
  }
248
234
 
249
235
  for param in data.parameters:
@@ -273,6 +259,12 @@ class Endpoint:
273
259
 
274
260
  unique_parameters.add(unique_param)
275
261
 
262
+ if any(
263
+ other_param for other_param in parameters_by_location[param.param_in] if other_param.name == param.name
264
+ ):
265
+ # Defined at the operation level, ignore it here
266
+ continue
267
+
276
268
  prop, new_schemas = property_from_data(
277
269
  name=param.name,
278
270
  required=param.required,
@@ -299,47 +291,69 @@ class Endpoint:
299
291
  location_error.data = param
300
292
  return location_error, schemas, parameters
301
293
 
302
- if prop.name in parameters_by_location[param.param_in]:
303
- # This parameter was defined in the Operation, so ignore the PathItem definition
294
+ # No reasons to use lazy imports in endpoints, so add lazy imports to relative here.
295
+ endpoint.relative_imports.update(prop.get_lazy_imports(prefix=models_relative_prefix))
296
+ endpoint.relative_imports.update(prop.get_imports(prefix=models_relative_prefix))
297
+ parameters_by_location[param.param_in].append(prop)
298
+
299
+ return endpoint._check_parameters_for_conflicts(config=config), schemas, parameters
300
+
301
+ def _check_parameters_for_conflicts(
302
+ self,
303
+ *,
304
+ config: Config,
305
+ previously_modified_params: Optional[Set[Tuple[oai.ParameterLocation, str]]] = None,
306
+ ) -> Union["Endpoint", ParseError]:
307
+ """Check for conflicting parameters
308
+
309
+ For parameters that have the same python_name but are in different locations, append the location to the
310
+ python_name. For parameters that have the same name but are in the same location, use their raw name without
311
+ snake casing instead.
312
+
313
+ Function stops when there's a conflict that can't be resolved or all parameters are guaranteed to have a
314
+ unique python_name.
315
+ """
316
+ modified_params = previously_modified_params or set()
317
+ used_python_names: Dict[PythonIdentifier, Tuple[oai.ParameterLocation, Property]] = {}
318
+ reserved_names = ["client", "url"]
319
+ for parameter in self.iter_all_parameters():
320
+ location, prop = parameter
321
+
322
+ if prop.python_name in reserved_names:
323
+ prop.set_python_name(new_name=f"{prop.python_name}_{location}", config=config)
324
+ modified_params.add((location, prop.name))
304
325
  continue
305
326
 
306
- for location, parameters_dict in parameters_by_location.items():
307
- if location == param.param_in or prop.name not in parameters_dict:
308
- continue
309
- existing_prop: Property = parameters_dict[prop.name]
310
- # Existing should be converted too for consistency
311
- endpoint.used_python_identifiers.discard(existing_prop.python_name)
312
- existing_prop.set_python_name(new_name=f"{existing_prop.name}_{location}", config=config)
313
-
314
- if existing_prop.python_name in endpoint.used_python_identifiers:
315
- return (
316
- ParseError(
317
- detail=f"Parameters with same Python identifier `{existing_prop.python_name}` detected",
318
- data=data,
319
- ),
320
- schemas,
321
- parameters,
322
- )
323
- endpoint.used_python_identifiers.add(existing_prop.python_name)
324
- prop.set_python_name(new_name=f"{param.name}_{param.param_in}", config=config)
327
+ conflicting = used_python_names.pop(prop.python_name, None)
328
+ if conflicting is None:
329
+ used_python_names[prop.python_name] = parameter
330
+ continue
331
+ conflicting_location, conflicting_prop = conflicting
332
+ if (conflicting_location, conflicting_prop.name) in modified_params or (
333
+ location,
334
+ prop.name,
335
+ ) in modified_params:
336
+ return ParseError(
337
+ detail=f"Parameters with same Python identifier {conflicting_prop.python_name} detected",
338
+ )
325
339
 
326
- if prop.python_name in endpoint.used_python_identifiers:
327
- return (
328
- ParseError(
329
- detail=f"Parameters with same Python identifier `{prop.python_name}` detected",
330
- data=data,
331
- ),
332
- schemas,
333
- parameters,
340
+ if location != conflicting_location:
341
+ conflicting_prop.set_python_name(
342
+ new_name=f"{conflicting_prop.python_name}_{conflicting_location}", config=config
334
343
  )
344
+ prop.set_python_name(new_name=f"{prop.python_name}_{location}", config=config)
345
+ elif conflicting_prop.name != prop.name: # Use the name to differentiate
346
+ conflicting_prop.set_python_name(new_name=conflicting_prop.name, config=config, skip_snake_case=True)
347
+ prop.set_python_name(new_name=prop.name, config=config, skip_snake_case=True)
335
348
 
336
- # No reasons to use lazy imports in endpoints, so add lazy imports to relative here.
337
- endpoint.relative_imports.update(prop.get_lazy_imports(prefix=models_relative_prefix))
338
- endpoint.relative_imports.update(prop.get_imports(prefix=models_relative_prefix))
339
- endpoint.used_python_identifiers.add(prop.python_name)
340
- parameters_by_location[param.param_in][prop.name] = prop
349
+ modified_params.add((location, conflicting_prop.name))
350
+ modified_params.add((conflicting_location, conflicting_prop.name))
351
+ used_python_names[prop.python_name] = parameter
352
+ used_python_names[conflicting_prop.python_name] = conflicting
341
353
 
342
- return endpoint, schemas, parameters
354
+ if len(modified_params) > 0 and modified_params != previously_modified_params:
355
+ return self._check_parameters_for_conflicts(config=config, previously_modified_params=modified_params)
356
+ return self
343
357
 
344
358
  @staticmethod
345
359
  def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]:
@@ -356,15 +370,13 @@ class Endpoint:
356
370
  endpoint = deepcopy(endpoint)
357
371
  parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path)
358
372
  try:
359
- sorted_params = sorted(
360
- endpoint.path_parameters.values(),
373
+ endpoint.path_parameters.sort(
361
374
  key=lambda param: parameters_from_path.index(param.name),
362
375
  )
363
- endpoint.path_parameters = OrderedDict((param.name, param) for param in sorted_params)
364
376
  except ValueError:
365
377
  pass # We're going to catch the difference down below
366
378
 
367
- if parameters_from_path != list(endpoint.path_parameters):
379
+ if parameters_from_path != [param.name for param in endpoint.path_parameters]:
368
380
  return ParseError(
369
381
  detail=f"Incorrect path templating for {endpoint.path} (Path parameters do not match with path)",
370
382
  )
@@ -442,17 +454,22 @@ class Endpoint:
442
454
  return self.responses[0].prop.get_type_string(quoted=False)
443
455
  return f"Union[{', '.join(types)}]"
444
456
 
445
- def iter_all_parameters(self) -> Iterator[Property]:
457
+ def iter_all_parameters(self) -> Iterator[Tuple[oai.ParameterLocation, Property]]:
446
458
  """Iterate through all the parameters of this endpoint"""
447
- yield from self.path_parameters.values()
448
- yield from self.query_parameters.values()
449
- yield from self.header_parameters.values()
450
- yield from self.cookie_parameters.values()
451
- yield from (body.prop for body in self.bodies)
459
+ yield from ((oai.ParameterLocation.PATH, param) for param in self.path_parameters)
460
+ yield from ((oai.ParameterLocation.QUERY, param) for param in self.query_parameters)
461
+ yield from ((oai.ParameterLocation.HEADER, param) for param in self.header_parameters)
462
+ yield from ((oai.ParameterLocation.COOKIE, param) for param in self.cookie_parameters)
452
463
 
453
464
  def list_all_parameters(self) -> List[Property]:
454
465
  """Return a List of all the parameters of this endpoint"""
455
- return list(self.iter_all_parameters())
466
+ return (
467
+ self.path_parameters
468
+ + self.query_parameters
469
+ + self.header_parameters
470
+ + self.cookie_parameters
471
+ + [body.prop for body in self.bodies]
472
+ )
456
473
 
457
474
 
458
475
  @dataclass
@@ -267,6 +267,17 @@ def _merge_properties(first: Property, second: Property) -> Property | PropertyE
267
267
  )
268
268
 
269
269
 
270
+ def _resolve_naming_conflict(first: Property, second: Property, config: Config) -> PropertyError | None:
271
+ first.set_python_name(first.name, config=config, skip_snake_case=True)
272
+ second.set_python_name(second.name, config=config, skip_snake_case=True)
273
+ if first.python_name == second.python_name:
274
+ return PropertyError(
275
+ header="Conflicting property names",
276
+ detail=f"Properties {first.name} and {second.name} have the same python_name",
277
+ )
278
+ return None
279
+
280
+
270
281
  class _PropertyData(NamedTuple):
271
282
  optional_props: list[Property]
272
283
  required_props: list[Property]
@@ -293,13 +304,23 @@ def _process_properties( # noqa: PLR0912, PLR0911
293
304
  def _add_if_no_conflict(new_prop: Property) -> PropertyError | None:
294
305
  nonlocal properties
295
306
 
296
- existing = properties.get(new_prop.name)
297
- merged_prop_or_error = _merge_properties(existing, new_prop) if existing else new_prop
307
+ name_conflict = properties.get(new_prop.name)
308
+ merged_prop_or_error = _merge_properties(name_conflict, new_prop) if name_conflict else new_prop
298
309
  if isinstance(merged_prop_or_error, PropertyError):
299
310
  merged_prop_or_error.header = (
300
311
  f"Found conflicting properties named {new_prop.name} when creating {class_name}"
301
312
  )
302
313
  return merged_prop_or_error
314
+
315
+ for other_prop in properties.values():
316
+ if other_prop.name == merged_prop_or_error.name:
317
+ continue # Same property, probably just got merged
318
+ if other_prop.python_name != merged_prop_or_error.python_name:
319
+ continue
320
+ naming_error = _resolve_naming_conflict(merged_prop_or_error, other_prop, config)
321
+ if naming_error is not None:
322
+ return naming_error
323
+
303
324
  properties[merged_prop_or_error.name] = merged_prop_or_error
304
325
  return None
305
326
 
@@ -67,14 +67,18 @@ class PropertyProtocol(Protocol):
67
67
  return ParseError(detail="Path parameter must be required")
68
68
  return None
69
69
 
70
- def set_python_name(self, new_name: str, config: Config) -> None:
70
+ def set_python_name(self, new_name: str, config: Config, skip_snake_case: bool = False) -> None:
71
71
  """Mutates this Property to set a new python_name.
72
72
 
73
73
  Required to mutate due to how Properties are stored and the difficulty of updating them in-dict.
74
74
  `new_name` will be validated before it is set, so `python_name` is not guaranteed to equal `new_name` after
75
75
  calling this.
76
76
  """
77
- object.__setattr__(self, "python_name", PythonIdentifier(value=new_name, prefix=config.field_prefix))
77
+ object.__setattr__(
78
+ self,
79
+ "python_name",
80
+ PythonIdentifier(value=new_name, prefix=config.field_prefix, skip_snake_case=skip_snake_case),
81
+ )
78
82
 
79
83
  def get_base_type_string(self, *, quoted: bool = False) -> str:
80
84
  """Get the string describing the Python type of this property. Base types no require quoting."""
@@ -5,7 +5,7 @@
5
5
  {% if endpoint.header_parameters or endpoint.bodies | length > 0 %}
6
6
  headers: Dict[str, Any] = {}
7
7
  {% if endpoint.header_parameters %}
8
- {% for parameter in endpoint.header_parameters.values() %}
8
+ {% for parameter in endpoint.header_parameters %}
9
9
  {% import "property_templates/" + parameter.template as param_template %}
10
10
  {% if param_template.transform_header %}
11
11
  {% set expression = param_template.transform_header(parameter.python_name) %}
@@ -22,7 +22,7 @@ headers: Dict[str, Any] = {}
22
22
  {% macro cookie_params(endpoint) %}
23
23
  {% if endpoint.cookie_parameters %}
24
24
  cookies = {}
25
- {% for parameter in endpoint.cookie_parameters.values() %}
25
+ {% for parameter in endpoint.cookie_parameters %}
26
26
  {% if parameter.required %}
27
27
  cookies["{{ parameter.name}}"] = {{ parameter.python_name }}
28
28
  {% else %}
@@ -39,7 +39,7 @@ if {{ parameter.python_name }} is not UNSET:
39
39
  {% if endpoint.query_parameters %}
40
40
  params: Dict[str, Any] = {}
41
41
 
42
- {% for property in endpoint.query_parameters.values() %}
42
+ {% for property in endpoint.query_parameters %}
43
43
  {% set destination = property.python_name %}
44
44
  {% import "property_templates/" + property.template as prop_template %}
45
45
  {% if prop_template.transform %}
@@ -91,7 +91,7 @@ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
91
91
  {# The all the kwargs passed into an endpoint (and variants thereof)) #}
92
92
  {% macro arguments(endpoint, include_client=True) %}
93
93
  {# path parameters #}
94
- {% for parameter in endpoint.path_parameters.values() %}
94
+ {% for parameter in endpoint.path_parameters %}
95
95
  {{ parameter.to_string() }},
96
96
  {% endfor %}
97
97
  {% if include_client or ((endpoint.list_all_parameters() | length) > (endpoint.path_parameters | length)) %}
@@ -116,21 +116,21 @@ body: Union[
116
116
  ],
117
117
  {% endif %}
118
118
  {# query parameters #}
119
- {% for parameter in endpoint.query_parameters.values() %}
119
+ {% for parameter in endpoint.query_parameters %}
120
120
  {{ parameter.to_string() }},
121
121
  {% endfor %}
122
- {% for parameter in endpoint.header_parameters.values() %}
122
+ {% for parameter in endpoint.header_parameters %}
123
123
  {{ parameter.to_string() }},
124
124
  {% endfor %}
125
125
  {# cookie parameters #}
126
- {% for parameter in endpoint.cookie_parameters.values() %}
126
+ {% for parameter in endpoint.cookie_parameters %}
127
127
  {{ parameter.to_string() }},
128
128
  {% endfor %}
129
129
  {% endmacro %}
130
130
 
131
131
  {# Just lists all kwargs to endpoints as name=name for passing to other functions #}
132
132
  {% macro kwargs(endpoint, include_client=True) %}
133
- {% for parameter in endpoint.path_parameters.values() %}
133
+ {% for parameter in endpoint.path_parameters %}
134
134
  {{ parameter.python_name }}={{ parameter.python_name }},
135
135
  {% endfor %}
136
136
  {% if include_client %}
@@ -139,13 +139,13 @@ client=client,
139
139
  {% if endpoint.bodies | length > 0 %}
140
140
  body=body,
141
141
  {% endif %}
142
- {% for parameter in endpoint.query_parameters.values() %}
142
+ {% for parameter in endpoint.query_parameters %}
143
143
  {{ parameter.python_name }}={{ parameter.python_name }},
144
144
  {% endfor %}
145
- {% for parameter in endpoint.header_parameters.values() %}
145
+ {% for parameter in endpoint.header_parameters %}
146
146
  {{ parameter.python_name }}={{ parameter.python_name }},
147
147
  {% endfor %}
148
- {% for parameter in endpoint.cookie_parameters.values() %}
148
+ {% for parameter in endpoint.cookie_parameters %}
149
149
  {{ parameter.python_name }}={{ parameter.python_name }},
150
150
  {% endfor %}
151
151
  {% endmacro %}
@@ -30,7 +30,7 @@ def _get_kwargs(
30
30
  "method": "{{ endpoint.method }}",
31
31
  {% if endpoint.path_parameters %}
32
32
  "url": "{{ endpoint.path }}".format(
33
- {%- for parameter in endpoint.path_parameters.values() -%}
33
+ {%- for parameter in endpoint.path_parameters -%}
34
34
  {{parameter.name}}={{parameter.python_name}},
35
35
  {%- endfor -%}
36
36
  ),
@@ -19,7 +19,7 @@ include = ["CHANGELOG.md", "{{ package_name }}/py.typed"]
19
19
 
20
20
  {% if pdm %}
21
21
  dependencies = [
22
- "httpx>=0.20.0,<0.27.0",
22
+ "httpx>=0.20.0,<0.28.0",
23
23
  "attrs>=21.3.0",
24
24
  "python-dateutil>=2.8.0",
25
25
  ]
@@ -31,7 +31,7 @@ package-type = "library"
31
31
 
32
32
  [tool.poetry.dependencies]
33
33
  python = "^3.8"
34
- httpx = ">=0.20.0,<0.27.0"
34
+ httpx = ">=0.20.0,<0.28.0"
35
35
  attrs = ">=21.3.0"
36
36
  python-dateutil = "^2.8.0"
37
37
  {% endif %}
@@ -1,3 +1,5 @@
1
1
  [tool.ruff]
2
- select = ["F", "I", "UP"]
3
2
  line-length = 120
3
+
4
+ [tool.ruff.lint]
5
+ select = ["F", "I", "UP"]
@@ -13,6 +13,6 @@ setup(
13
13
  long_description_content_type="text/markdown",
14
14
  packages=find_packages(),
15
15
  python_requires=">=3.8, <4",
16
- install_requires=["httpx >= 0.20.0, < 0.27.0", "attrs >= 21.3.0", "python-dateutil >= 2.8.0, < 3"],
16
+ install_requires=["httpx >= 0.20.0, < 0.28.0", "attrs >= 21.3.0", "python-dateutil >= 2.8.0, < 3"],
17
17
  package_data={"{{ package_name }}": ["py.typed"]},
18
18
  )
@@ -12,8 +12,11 @@ DELIMITERS = r"\. _-"
12
12
  class PythonIdentifier(str):
13
13
  """A snake_case string which has been validated / transformed into a valid identifier for Python"""
14
14
 
15
- def __new__(cls, value: str, prefix: str) -> PythonIdentifier:
16
- new_value = fix_reserved_words(snake_case(sanitize(value)))
15
+ def __new__(cls, value: str, prefix: str, skip_snake_case: bool = False) -> PythonIdentifier:
16
+ new_value = sanitize(value)
17
+ if not skip_snake_case:
18
+ new_value = snake_case(new_value)
19
+ new_value = fix_reserved_words(new_value)
17
20
 
18
21
  if not new_value.isidentifier() or value.startswith("_"):
19
22
  new_value = f"{prefix}{new_value}"
@@ -12,13 +12,13 @@ dependencies = [
12
12
  "pydantic>=2.1.1,<3.0.0",
13
13
  "attrs>=21.3.0",
14
14
  "python-dateutil>=2.8.1,<3.0.0",
15
- "httpx>=0.20.0,<0.27.0",
15
+ "httpx>=0.20.0,<0.28.0",
16
16
  "PyYAML>=6.0,<7.0",
17
- "ruff>=0.1.2,<1.0.0",
17
+ "ruff>=0.2,<0.3",
18
18
  "typing-extensions>=4.8.0,<5.0.0",
19
19
  ]
20
20
  name = "openapi-python-client"
21
- version = "0.17.3"
21
+ version = "0.18.0"
22
22
  description = "Generate modern Python clients from OpenAPI"
23
23
  keywords = [
24
24
  "OpenAPI",
@@ -47,7 +47,6 @@ repository = "https://github.com/openapi-generators/openapi-python-client"
47
47
  openapi-python-client = "openapi_python_client.cli:app"
48
48
 
49
49
  [tool.ruff]
50
- select = ["E", "F", "I", "UP", "B", "PL", "RUF"]
51
50
  line-length = 120
52
51
  exclude = [
53
52
  ".git",
@@ -57,9 +56,12 @@ exclude = [
57
56
  "end_to_end_tests/*",
58
57
  "tests/test_templates/*",
59
58
  ]
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "I", "UP", "B", "PL", "RUF"]
60
62
  ignore = ["E501", "PLR0913"]
61
63
 
62
- [tool.ruff.per-file-ignores]
64
+ [tool.ruff.lint.per-file-ignores]
63
65
  "openapi_python_client/cli.py" = ["B008"]
64
66
 
65
67
  [tool.coverage.run]