openapi-python-client 0.21.3__tar.gz → 0.21.5__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 (106) hide show
  1. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/.gitignore +3 -0
  2. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/PKG-INFO +1 -1
  3. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/bodies.py +1 -0
  4. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/openapi.py +2 -2
  5. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/__init__.py +16 -8
  6. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/any.py +6 -2
  7. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/boolean.py +3 -3
  8. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/const.py +7 -9
  9. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/date.py +1 -1
  10. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/datetime.py +1 -1
  11. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/enum_property.py +2 -2
  12. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/float.py +3 -3
  13. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/int.py +10 -6
  14. openapi_python_client-0.21.5/openapi_python_client/parser/properties/merge_properties.py +168 -0
  15. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/model_property.py +19 -68
  16. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/none.py +1 -1
  17. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/protocol.py +12 -4
  18. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/schemas.py +3 -0
  19. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/string.py +1 -5
  20. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/schema.py +31 -4
  21. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/model.py.jinja +2 -0
  22. openapi_python_client-0.21.5/openapi_python_client/templates/property_templates/const_property.py.jinja +5 -0
  23. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/pyproject.toml +4 -3
  24. openapi_python_client-0.21.3/openapi_python_client/templates/property_templates/const_property.py.jinja +0 -5
  25. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/LICENSE +0 -0
  26. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/README.md +0 -0
  27. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/__init__.py +0 -0
  28. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/__main__.py +0 -0
  29. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/cli.py +0 -0
  30. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/config.py +0 -0
  31. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/__init__.py +0 -0
  32. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/errors.py +0 -0
  33. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/file.py +0 -0
  34. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/list_property.py +0 -0
  35. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/property.py +0 -0
  36. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/union.py +0 -0
  37. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/parser/responses.py +0 -0
  38. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/py.typed +0 -0
  39. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/3.0.3.md +0 -0
  40. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/3.1.0.md +0 -0
  41. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/__init__.py +0 -0
  42. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/data_type.py +0 -0
  43. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/LICENSE +0 -0
  44. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/README.md +0 -0
  45. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/__init__.py +0 -0
  46. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/callback.py +0 -0
  47. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/components.py +0 -0
  48. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/contact.py +0 -0
  49. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +0 -0
  50. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/encoding.py +0 -0
  51. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/example.py +0 -0
  52. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +0 -0
  53. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/header.py +0 -0
  54. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/info.py +0 -0
  55. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/license.py +0 -0
  56. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/link.py +0 -0
  57. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/media_type.py +0 -0
  58. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +0 -0
  59. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py +0 -0
  60. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/open_api.py +0 -0
  61. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/operation.py +0 -0
  62. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/parameter.py +0 -0
  63. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/path_item.py +0 -0
  64. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/paths.py +0 -0
  65. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/reference.py +0 -0
  66. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/request_body.py +0 -0
  67. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/response.py +0 -0
  68. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/responses.py +0 -0
  69. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +0 -0
  70. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +0 -0
  71. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/server.py +0 -0
  72. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +0 -0
  73. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/tag.py +0 -0
  74. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/xml.py +0 -0
  75. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/schema/parameter_location.py +0 -0
  76. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/.gitignore.jinja +0 -0
  77. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/README.md.jinja +0 -0
  78. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/api_init.py.jinja +0 -0
  79. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/client.py.jinja +0 -0
  80. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/endpoint_init.py.jinja +0 -0
  81. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/endpoint_macros.py.jinja +0 -0
  82. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/endpoint_module.py.jinja +0 -0
  83. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/errors.py.jinja +0 -0
  84. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/helpers.jinja +0 -0
  85. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/int_enum.py.jinja +0 -0
  86. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/models_init.py.jinja +0 -0
  87. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/package_init.py.jinja +0 -0
  88. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/any_property.py.jinja +0 -0
  89. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/boolean_property.py.jinja +0 -0
  90. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/date_property.py.jinja +0 -0
  91. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/datetime_property.py.jinja +0 -0
  92. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/enum_property.py.jinja +0 -0
  93. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/file_property.py.jinja +0 -0
  94. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/float_property.py.jinja +0 -0
  95. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/helpers.jinja +0 -0
  96. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/int_property.py.jinja +0 -0
  97. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/list_property.py.jinja +0 -0
  98. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/model_property.py.jinja +0 -0
  99. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/property_macros.py.jinja +0 -0
  100. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/union_property.py.jinja +0 -0
  101. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/pyproject.toml.jinja +0 -0
  102. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/pyproject_ruff.toml.jinja +0 -0
  103. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/setup.py.jinja +0 -0
  104. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/str_enum.py.jinja +0 -0
  105. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/templates/types.py.jinja +0 -0
  106. {openapi_python_client-0.21.3 → openapi_python_client-0.21.5}/openapi_python_client/utils.py +0 -0
@@ -24,6 +24,9 @@ dmypy.json
24
24
  # JetBrains
25
25
  .idea/
26
26
 
27
+ # Visual Studio Code
28
+ .vscode/
29
+
27
30
  test-reports/
28
31
 
29
32
  /coverage.xml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openapi-python-client
3
- Version: 0.21.3
3
+ Version: 0.21.5
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>
@@ -117,6 +117,7 @@ def body_from_data(
117
117
  **schemas.classes_by_name,
118
118
  prop.class_info.name: prop,
119
119
  },
120
+ models_to_process=[*schemas.models_to_process, prop],
120
121
  )
121
122
  bodies.append(
122
123
  Body(
@@ -155,7 +155,7 @@ class Endpoint:
155
155
  ParseError(
156
156
  detail=(
157
157
  f"Invalid response status code {code} (not a valid HTTP "
158
- f"status code), response will be ommitted from generated "
158
+ f"status code), response will be omitted from generated "
159
159
  f"client"
160
160
  )
161
161
  )
@@ -175,7 +175,7 @@ class Endpoint:
175
175
  ParseError(
176
176
  detail=(
177
177
  f"Cannot parse response for status code {status_code}{detail_suffix}, "
178
- f"response will be ommitted from generated client"
178
+ f"response will be omitted from generated client"
179
179
  ),
180
180
  data=response.data,
181
181
  )
@@ -83,7 +83,6 @@ def _string_based_property(
83
83
  name=name,
84
84
  default=data.default,
85
85
  required=required,
86
- pattern=data.pattern,
87
86
  python_name=python_name,
88
87
  description=data.description,
89
88
  example=data.example,
@@ -126,7 +125,7 @@ def _property_from_ref(
126
125
  return prop, schemas
127
126
 
128
127
 
129
- def property_from_data( # noqa: PLR0911
128
+ def property_from_data( # noqa: PLR0911, PLR0912
130
129
  name: str,
131
130
  required: bool,
132
131
  data: oai.Reference | oai.Schema,
@@ -153,7 +152,7 @@ def property_from_data( # noqa: PLR0911
153
152
  sub_data: list[oai.Schema | oai.Reference] = data.allOf + data.anyOf + data.oneOf
154
153
  # A union of a single reference should just be passed through to that reference (don't create copy class)
155
154
  if len(sub_data) == 1 and isinstance(sub_data[0], oai.Reference):
156
- return _property_from_ref(
155
+ prop, schemas = _property_from_ref(
157
156
  name=name,
158
157
  required=required,
159
158
  parent=data,
@@ -162,6 +161,16 @@ def property_from_data( # noqa: PLR0911
162
161
  config=config,
163
162
  roots=roots,
164
163
  )
164
+ # We won't be generating a separate Python class for this schema - references to it will just use
165
+ # the class for the schema it's referencing - so we don't add it to classes_by_name; but we do
166
+ # add it to models_to_process, if it's a model, because its properties still need to be resolved.
167
+ if isinstance(prop, ModelProperty):
168
+ schemas = evolve(
169
+ schemas,
170
+ models_to_process=[*schemas.models_to_process, prop],
171
+ )
172
+ return prop, schemas
173
+
165
174
  if data.type == oai.DataType.BOOLEAN:
166
175
  return (
167
176
  BooleanProperty.build(
@@ -271,7 +280,7 @@ def property_from_data( # noqa: PLR0911
271
280
  AnyProperty.build(
272
281
  name=name,
273
282
  required=required,
274
- default=None,
283
+ default=data.default,
275
284
  python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
276
285
  description=data.description,
277
286
  example=data.example,
@@ -341,7 +350,7 @@ def _process_model_errors(
341
350
 
342
351
 
343
352
  def _process_models(*, schemas: Schemas, config: Config) -> Schemas:
344
- to_process = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, ModelProperty))
353
+ to_process = schemas.models_to_process
345
354
  still_making_progress = True
346
355
  final_model_errors: list[tuple[ModelProperty, PropertyError]] = []
347
356
  latest_model_errors: list[tuple[ModelProperty, PropertyError]] = []
@@ -368,12 +377,11 @@ def _process_models(*, schemas: Schemas, config: Config) -> Schemas:
368
377
  continue
369
378
  schemas = schemas_or_err
370
379
  still_making_progress = True
371
- to_process = (prop for prop in next_round)
380
+ to_process = next_round
372
381
 
373
382
  final_model_errors.extend(latest_model_errors)
374
383
  errors = _process_model_errors(final_model_errors, schemas=schemas)
375
- schemas.errors.extend(errors)
376
- return schemas
384
+ return evolve(schemas, errors=[*schemas.errors, *errors], models_to_process=to_process)
377
385
 
378
386
 
379
387
  def build_schemas(
@@ -33,9 +33,13 @@ class AnyProperty(PropertyProtocol):
33
33
 
34
34
  @classmethod
35
35
  def convert_value(cls, value: Any) -> Value | None:
36
- if value is None or isinstance(value, Value):
36
+ from .string import StringProperty
37
+
38
+ if value is None:
37
39
  return value
38
- return Value(str(value))
40
+ if isinstance(value, str):
41
+ return StringProperty.convert_value(value)
42
+ return Value(python_code=str(value), raw_value=value)
39
43
 
40
44
  name: str
41
45
  required: bool
@@ -59,9 +59,9 @@ class BooleanProperty(PropertyProtocol):
59
59
  return value
60
60
  if isinstance(value, str):
61
61
  if value.lower() == "true":
62
- return Value("True")
62
+ return Value(python_code="True", raw_value=value)
63
63
  elif value.lower() == "false":
64
- return Value("False")
64
+ return Value(python_code="False", raw_value=value)
65
65
  if isinstance(value, bool):
66
- return Value(str(value))
66
+ return Value(python_code=str(value), raw_value=value)
67
67
  return PropertyError(f"Invalid boolean value: {value}")
@@ -27,7 +27,7 @@ class ConstProperty(PropertyProtocol):
27
27
  def build(
28
28
  cls,
29
29
  *,
30
- const: str | int,
30
+ const: str | int | float | bool,
31
31
  default: Any,
32
32
  name: str,
33
33
  python_name: PythonIdentifier,
@@ -63,13 +63,13 @@ class ConstProperty(PropertyProtocol):
63
63
  return prop
64
64
 
65
65
  def convert_value(self, value: Any) -> Value | None | PropertyError:
66
- if isinstance(value, Value):
67
- return value
68
66
  value = self._convert_value(value)
69
67
  if value is None:
70
68
  return value
71
69
  if value != self.value:
72
- return PropertyError(detail=f"Invalid value for const {self.name}; {value} != {self.value}")
70
+ return PropertyError(
71
+ detail=f"Invalid value for const {self.name}; {value.raw_value} != {self.value.raw_value}"
72
+ )
73
73
  return value
74
74
 
75
75
  @staticmethod
@@ -85,11 +85,9 @@ class ConstProperty(PropertyProtocol):
85
85
  def _convert_value(value: Any) -> Value | None:
86
86
  if value is None or isinstance(value, Value):
87
87
  return value
88
- if isinstance(value, Value):
89
- return value # pragma: no cover
90
88
  if isinstance(value, str):
91
89
  return StringProperty.convert_value(value)
92
- return Value(str(value))
90
+ return Value(python_code=str(value), raw_value=value)
93
91
 
94
92
  def get_type_string(
95
93
  self,
@@ -99,7 +97,7 @@ class ConstProperty(PropertyProtocol):
99
97
  multipart: bool = False,
100
98
  quoted: bool = False,
101
99
  ) -> str:
102
- lit = f"Literal[{self.value}]"
100
+ lit = f"Literal[{self.value.python_code}]"
103
101
  if not no_optional and not self.required:
104
102
  return f"Union[{lit}, Unset]"
105
103
  return lit
@@ -115,6 +113,6 @@ class ConstProperty(PropertyProtocol):
115
113
  if self.required:
116
114
  return {"from typing import Literal"}
117
115
  return {
118
- "from typing import Literal, Union",
116
+ "from typing import Literal, Union, cast",
119
117
  f"from {prefix}types import UNSET, Unset",
120
118
  }
@@ -57,7 +57,7 @@ class DateProperty(PropertyProtocol):
57
57
  isoparse(value).date() # make sure it's a valid value
58
58
  except ValueError as e:
59
59
  return PropertyError(f"Invalid date: {e}")
60
- return Value(f"isoparse({value!r}).date()")
60
+ return Value(python_code=f"isoparse({value!r}).date()", raw_value=value)
61
61
  return PropertyError(f"Cannot convert {value} to a date")
62
62
 
63
63
  def get_imports(self, *, prefix: str) -> set[str]:
@@ -59,7 +59,7 @@ class DateTimeProperty(PropertyProtocol):
59
59
  isoparse(value) # make sure it's a valid value
60
60
  except ValueError as e:
61
61
  return PropertyError(f"Invalid datetime: {e}")
62
- return Value(f"isoparse({value!r})")
62
+ return Value(python_code=f"isoparse({value!r})", raw_value=value)
63
63
  return PropertyError(f"Cannot convert {value} to a datetime")
64
64
 
65
65
  def get_imports(self, *, prefix: str) -> set[str]:
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __all__ = ["EnumProperty"]
3
+ __all__ = ["EnumProperty", "ValueType"]
4
4
 
5
5
  from typing import Any, ClassVar, List, Union, cast
6
6
 
@@ -159,7 +159,7 @@ class EnumProperty(PropertyProtocol):
159
159
  if isinstance(value, self.value_type):
160
160
  inverse_values = {v: k for k, v in self.values.items()}
161
161
  try:
162
- return Value(f"{self.class_info.name}.{inverse_values[value]}")
162
+ return Value(python_code=f"{self.class_info.name}.{inverse_values[value]}", raw_value=value)
163
163
  except KeyError:
164
164
  return PropertyError(detail=f"Value {value} is not valid for enum {self.name}")
165
165
  return PropertyError(detail=f"Cannot convert {value} to enum {self.name} of type {self.value_type}")
@@ -61,11 +61,11 @@ class FloatProperty(PropertyProtocol):
61
61
  if isinstance(value, str):
62
62
  try:
63
63
  parsed = float(value)
64
- return Value(str(parsed))
64
+ return Value(python_code=str(parsed), raw_value=value)
65
65
  except ValueError:
66
66
  return PropertyError(f"Invalid float value: {value}")
67
67
  if isinstance(value, float):
68
- return Value(str(value))
68
+ return Value(python_code=str(value), raw_value=value)
69
69
  if isinstance(value, int) and not isinstance(value, bool):
70
- return Value(str(float(value)))
70
+ return Value(python_code=str(float(value)), raw_value=value)
71
71
  return PropertyError(f"Cannot convert {value} to a float")
@@ -58,12 +58,16 @@ class IntProperty(PropertyProtocol):
58
58
  def convert_value(cls, value: Any) -> Value | None | PropertyError:
59
59
  if value is None or isinstance(value, Value):
60
60
  return value
61
- if isinstance(value, str):
61
+ converted = value
62
+ if isinstance(converted, str):
62
63
  try:
63
- int(value)
64
+ converted = float(converted)
64
65
  except ValueError:
65
- return PropertyError(f"Invalid int value: {value}")
66
- return Value(value)
67
- if isinstance(value, int) and not isinstance(value, bool):
68
- return Value(str(value))
66
+ return PropertyError(f"Invalid int value: {converted}")
67
+ if isinstance(converted, float):
68
+ as_int = int(converted)
69
+ if converted == as_int:
70
+ converted = as_int
71
+ if isinstance(converted, int) and not isinstance(converted, bool):
72
+ return Value(python_code=str(converted), raw_value=value)
69
73
  return PropertyError(f"Invalid int value: {value}")
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ from openapi_python_client.parser.properties.date import DateProperty
4
+ from openapi_python_client.parser.properties.datetime import DateTimeProperty
5
+ from openapi_python_client.parser.properties.file import FileProperty
6
+
7
+ __all__ = ["merge_properties"]
8
+
9
+ from typing import TypeVar, cast
10
+
11
+ from attr import evolve
12
+
13
+ from ..errors import PropertyError
14
+ from . import FloatProperty
15
+ from .any import AnyProperty
16
+ from .enum_property import EnumProperty
17
+ from .int import IntProperty
18
+ from .list_property import ListProperty
19
+ from .property import Property
20
+ from .protocol import PropertyProtocol
21
+ from .string import StringProperty
22
+
23
+ PropertyT = TypeVar("PropertyT", bound=PropertyProtocol)
24
+
25
+
26
+ STRING_WITH_FORMAT_TYPES = (DateProperty, DateTimeProperty, FileProperty)
27
+
28
+
29
+ def merge_properties(prop1: Property, prop2: Property) -> Property | PropertyError: # noqa: PLR0911
30
+ """Attempt to create a new property that incorporates the behavior of both.
31
+
32
+ This is used when merging schemas with allOf, when two schemas define a property with the same name.
33
+
34
+ OpenAPI defines allOf in terms of validation behavior: the input must pass the validation rules
35
+ defined in all the listed schemas. Our task here is slightly more difficult, since we must end
36
+ up with a single Property object that will be used to generate a single class property in the
37
+ generated code. Due to limitations of our internal model, this may not be possible for some
38
+ combinations of property attributes that OpenAPI supports (for instance, we have no way to represent
39
+ a string property that must match two different regexes).
40
+
41
+ Properties can also have attributes that do not represent validation rules, such as "description"
42
+ and "example". OpenAPI does not define any overriding/aggregation rules for these in allOf. The
43
+ implementation here is, assuming prop1 and prop2 are in the same order that the schemas were in the
44
+ allOf, any such attributes that prop2 specifies will override the ones from prop1.
45
+ """
46
+ if isinstance(prop2, AnyProperty):
47
+ return _merge_common_attributes(prop1, prop2)
48
+
49
+ if isinstance(prop1, AnyProperty):
50
+ # Use the base type of `prop2`, but keep the override order
51
+ return _merge_common_attributes(prop2, prop1, prop2)
52
+
53
+ if isinstance(prop1, EnumProperty) or isinstance(prop2, EnumProperty):
54
+ return _merge_with_enum(prop1, prop2)
55
+
56
+ if (merged := _merge_same_type(prop1, prop2)) is not None:
57
+ return merged
58
+
59
+ if (merged := _merge_numeric(prop1, prop2)) is not None:
60
+ return merged
61
+
62
+ if (merged := _merge_string_with_format(prop1, prop2)) is not None:
63
+ return merged
64
+
65
+ return PropertyError(
66
+ detail=f"{prop1.get_type_string(no_optional=True)} can't be merged with {prop2.get_type_string(no_optional=True)}"
67
+ )
68
+
69
+
70
+ def _merge_same_type(prop1: Property, prop2: Property) -> Property | None | PropertyError:
71
+ if type(prop1) is not type(prop2):
72
+ return None
73
+
74
+ if prop1 == prop2:
75
+ # It's always OK to redefine a property with everything exactly the same
76
+ return prop1
77
+
78
+ if isinstance(prop1, ListProperty) and isinstance(prop2, ListProperty):
79
+ inner_property = merge_properties(prop1.inner_property, prop2.inner_property) # type: ignore
80
+ if isinstance(inner_property, PropertyError):
81
+ return PropertyError(detail=f"can't merge list properties: {inner_property.detail}")
82
+ prop1.inner_property = inner_property
83
+
84
+ # For all other property types, there aren't any special attributes that affect validation, so just
85
+ # apply the rules for common attributes like "description".
86
+ return _merge_common_attributes(prop1, prop2)
87
+
88
+
89
+ def _merge_string_with_format(prop1: Property, prop2: Property) -> Property | None | PropertyError:
90
+ """Merge a string that has no format with a string that has a format"""
91
+ # Here we need to use the DateProperty/DateTimeProperty/FileProperty as the base so that we preserve
92
+ # its class, but keep the correct override order for merging the attributes.
93
+ if isinstance(prop1, StringProperty) and isinstance(prop2, STRING_WITH_FORMAT_TYPES):
94
+ # Use the more specific class as a base, but keep the correct override order
95
+ return _merge_common_attributes(prop2, prop1, prop2)
96
+ elif isinstance(prop2, StringProperty) and isinstance(prop1, STRING_WITH_FORMAT_TYPES):
97
+ return _merge_common_attributes(prop1, prop2)
98
+ else:
99
+ return None
100
+
101
+
102
+ def _merge_numeric(prop1: Property, prop2: Property) -> IntProperty | None | PropertyError:
103
+ """Merge IntProperty with FloatProperty"""
104
+ if isinstance(prop1, IntProperty) and isinstance(prop2, (IntProperty, FloatProperty)):
105
+ return _merge_common_attributes(prop1, prop2)
106
+ elif isinstance(prop2, IntProperty) and isinstance(prop1, (IntProperty, FloatProperty)):
107
+ # Use the IntProperty as a base since it's more restrictive, but keep the correct override order
108
+ return _merge_common_attributes(prop2, prop1, prop2)
109
+ else:
110
+ return None
111
+
112
+
113
+ def _merge_with_enum(prop1: PropertyProtocol, prop2: PropertyProtocol) -> EnumProperty | PropertyError:
114
+ if isinstance(prop1, EnumProperty) and isinstance(prop2, EnumProperty):
115
+ # We want the narrowest validation rules that fit both, so use whichever values list is a
116
+ # subset of the other.
117
+ if _values_are_subset(prop1, prop2):
118
+ values = prop1.values
119
+ class_info = prop1.class_info
120
+ elif _values_are_subset(prop2, prop1):
121
+ values = prop2.values
122
+ class_info = prop2.class_info
123
+ else:
124
+ return PropertyError(detail="can't redefine an enum property with incompatible lists of values")
125
+ return _merge_common_attributes(evolve(prop1, values=values, class_info=class_info), prop2)
126
+
127
+ # If enum values were specified for just one of the properties, use those.
128
+ enum_prop = prop1 if isinstance(prop1, EnumProperty) else cast(EnumProperty, prop2)
129
+ non_enum_prop = prop2 if isinstance(prop1, EnumProperty) else prop1
130
+ if (isinstance(non_enum_prop, IntProperty) and enum_prop.value_type is int) or (
131
+ isinstance(non_enum_prop, StringProperty) and enum_prop.value_type is str
132
+ ):
133
+ return _merge_common_attributes(enum_prop, prop1, prop2)
134
+ return PropertyError(
135
+ detail=f"can't combine enum of type {enum_prop.value_type} with {non_enum_prop.get_type_string(no_optional=True)}"
136
+ )
137
+
138
+
139
+ def _merge_common_attributes(base: PropertyT, *extend_with: PropertyProtocol) -> PropertyT | PropertyError:
140
+ """Create a new instance based on base, overriding basic attributes with values from extend_with, in order.
141
+
142
+ For "default", "description", and "example", a non-None value overrides any value from a previously
143
+ specified property. The behavior is similar to using the spread operator with dictionaries, except
144
+ that None means "not specified".
145
+
146
+ For "required", any True value overrides all other values (a property that was previously required
147
+ cannot become optional).
148
+ """
149
+ current = base
150
+ for override in extend_with:
151
+ if override.default is not None:
152
+ override_default = current.convert_value(override.default.raw_value)
153
+ else:
154
+ override_default = None
155
+ if isinstance(override_default, PropertyError):
156
+ return override_default
157
+ current = evolve(
158
+ current, # type: ignore # can't prove that every property type is an attrs class, but it is
159
+ required=current.required or override.required,
160
+ default=override_default or current.default,
161
+ description=override.description or current.description,
162
+ example=override.example or current.example,
163
+ )
164
+ return current
165
+
166
+
167
+ def _values_are_subset(prop1: EnumProperty, prop2: EnumProperty) -> bool:
168
+ return set(prop1.values.items()) <= set(prop2.values.items())
@@ -10,7 +10,6 @@ from ... import schema as oai
10
10
  from ...utils import PythonIdentifier
11
11
  from ..errors import ParseError, PropertyError
12
12
  from .any import AnyProperty
13
- from .enum_property import EnumProperty
14
13
  from .protocol import PropertyProtocol, Value
15
14
  from .schemas import Class, ReferencePath, Schemas, parse_reference_path
16
15
 
@@ -119,7 +118,11 @@ class ModelProperty(PropertyProtocol):
119
118
  )
120
119
  return error, schemas
121
120
 
122
- schemas = evolve(schemas, classes_by_name={**schemas.classes_by_name, class_info.name: prop})
121
+ schemas = evolve(
122
+ schemas,
123
+ classes_by_name={**schemas.classes_by_name, class_info.name: prop},
124
+ models_to_process=[*schemas.models_to_process, prop],
125
+ )
123
126
  return prop, schemas
124
127
 
125
128
  @classmethod
@@ -216,59 +219,6 @@ class ModelProperty(PropertyProtocol):
216
219
  from .property import Property # noqa: E402
217
220
 
218
221
 
219
- def _values_are_subset(first: EnumProperty, second: EnumProperty) -> bool:
220
- return set(first.values.items()) <= set(second.values.items())
221
-
222
-
223
- def _types_are_subset(first: EnumProperty, second: Property) -> bool:
224
- from . import IntProperty, StringProperty
225
-
226
- if first.value_type is int and isinstance(second, IntProperty):
227
- return True
228
- if first.value_type is str and isinstance(second, StringProperty):
229
- return True
230
- return False
231
-
232
-
233
- def _enum_subset(first: Property, second: Property) -> EnumProperty | None:
234
- """Return the EnumProperty that is the subset of the other, if possible."""
235
-
236
- if isinstance(first, EnumProperty):
237
- if isinstance(second, EnumProperty):
238
- if _values_are_subset(first, second):
239
- return first
240
- if _values_are_subset(second, first):
241
- return second
242
- return None
243
- return first if _types_are_subset(first, second) else None
244
-
245
- if isinstance(second, EnumProperty) and _types_are_subset(second, first):
246
- return second
247
- return None
248
-
249
-
250
- def _merge_properties(first: Property, second: Property) -> Property | PropertyError:
251
- required = first.required or second.required
252
-
253
- err = None
254
-
255
- if first.__class__ == second.__class__:
256
- first = evolve(first, required=required)
257
- second = evolve(second, required=required)
258
- if first == second:
259
- return first
260
- err = PropertyError(header="Cannot merge properties", detail="Properties has conflicting values")
261
-
262
- enum_subset = _enum_subset(first, second)
263
- if enum_subset is not None:
264
- return evolve(enum_subset, required=required)
265
-
266
- return err or PropertyError(
267
- header="Cannot merge properties",
268
- detail=f"{first.__class__}, {second.__class__}Properties have incompatible types",
269
- )
270
-
271
-
272
222
  def _resolve_naming_conflict(first: Property, second: Property, config: Config) -> PropertyError | None:
273
223
  first.set_python_name(first.name, config=config, skip_snake_case=True)
274
224
  second.set_python_name(second.name, config=config, skip_snake_case=True)
@@ -297,6 +247,7 @@ def _process_properties( # noqa: PLR0912, PLR0911
297
247
  roots: set[ReferencePath | utils.ClassName],
298
248
  ) -> _PropertyData | PropertyError:
299
249
  from . import property_from_data
250
+ from .merge_properties import merge_properties
300
251
 
301
252
  properties: dict[str, Property] = {}
302
253
  relative_imports: set[str] = set()
@@ -307,26 +258,26 @@ def _process_properties( # noqa: PLR0912, PLR0911
307
258
  nonlocal properties
308
259
 
309
260
  name_conflict = properties.get(new_prop.name)
310
- merged_prop_or_error = _merge_properties(name_conflict, new_prop) if name_conflict else new_prop
311
- if isinstance(merged_prop_or_error, PropertyError):
312
- merged_prop_or_error.header = (
313
- f"Found conflicting properties named {new_prop.name} when creating {class_name}"
314
- )
315
- return merged_prop_or_error
261
+ merged_prop = merge_properties(name_conflict, new_prop) if name_conflict else new_prop
262
+ if isinstance(merged_prop, PropertyError):
263
+ merged_prop.header = f"Found conflicting properties named {new_prop.name} when creating {class_name}"
264
+ return merged_prop
316
265
 
317
266
  for other_prop in properties.values():
318
- if other_prop.name == merged_prop_or_error.name:
267
+ if other_prop.name == merged_prop.name:
319
268
  continue # Same property, probably just got merged
320
- if other_prop.python_name != merged_prop_or_error.python_name:
269
+ if other_prop.python_name != merged_prop.python_name:
321
270
  continue
322
- naming_error = _resolve_naming_conflict(merged_prop_or_error, other_prop, config)
271
+ naming_error = _resolve_naming_conflict(merged_prop, other_prop, config)
323
272
  if naming_error is not None:
324
273
  return naming_error
325
274
 
326
- properties[merged_prop_or_error.name] = merged_prop_or_error
275
+ properties[merged_prop.name] = merged_prop
327
276
  return None
328
277
 
329
- unprocessed_props = data.properties or {}
278
+ unprocessed_props: list[tuple[str, oai.Reference | oai.Schema]] = (
279
+ list(data.properties.items()) if data.properties else []
280
+ )
330
281
  for sub_prop in data.allOf:
331
282
  if isinstance(sub_prop, oai.Reference):
332
283
  ref_path = parse_reference_path(sub_prop.ref)
@@ -348,10 +299,10 @@ def _process_properties( # noqa: PLR0912, PLR0911
348
299
  return err
349
300
  schemas.add_dependencies(ref_path=ref_path, roots=roots)
350
301
  else:
351
- unprocessed_props.update(sub_prop.properties or {})
302
+ unprocessed_props.extend(sub_prop.properties.items() if sub_prop.properties else [])
352
303
  required_set.update(sub_prop.required or [])
353
304
 
354
- for key, value in unprocessed_props.items():
305
+ for key, value in unprocessed_props:
355
306
  prop_required = key in required_set
356
307
  prop_or_error: Property | (PropertyError | None)
357
308
  prop_or_error, schemas = property_from_data(
@@ -57,5 +57,5 @@ class NoneProperty(PropertyProtocol):
57
57
  return value
58
58
  if isinstance(value, str):
59
59
  if value == "None":
60
- return Value(value)
60
+ return Value(python_code=value, raw_value=value)
61
61
  return PropertyError(f"Value {value} is not valid, only None is allowed")
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  __all__ = ["PropertyProtocol", "Value"]
4
4
 
5
5
  from abc import abstractmethod
6
+ from dataclasses import dataclass
6
7
  from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar
7
8
 
8
9
  from ... import Config
@@ -16,8 +17,15 @@ else:
16
17
  ModelProperty = "ModelProperty"
17
18
 
18
19
 
19
- class Value(str):
20
- """Represents a valid (converted) value for a property"""
20
+ @dataclass
21
+ class Value:
22
+ """
23
+ Some literal values in OpenAPI documents (like defaults) have to be converted into Python code safely
24
+ (with string escaping, for example). We still keep the `raw_value` around for merging `allOf`.
25
+ """
26
+
27
+ python_code: str
28
+ raw_value: Any
21
29
 
22
30
 
23
31
  PropertyType = TypeVar("PropertyType", bound="PropertyProtocol")
@@ -148,7 +156,7 @@ class PropertyProtocol(Protocol):
148
156
  """How this should be declared in a dataclass"""
149
157
  default: str | None
150
158
  if self.default is not None:
151
- default = self.default
159
+ default = self.default.python_code
152
160
  elif not self.required:
153
161
  default = "UNSET"
154
162
  else:
@@ -162,7 +170,7 @@ class PropertyProtocol(Protocol):
162
170
  """Returns property docstring"""
163
171
  doc = f"{self.python_name} ({self.get_type_string()}): {self.description or ''}"
164
172
  if self.default:
165
- doc += f" Default: {self.default}."
173
+ doc += f" Default: {self.default.python_code}."
166
174
  if self.example:
167
175
  doc += f" Example: {self.example}."
168
176
  return doc
@@ -22,8 +22,10 @@ from ...utils import ClassName, PythonIdentifier
22
22
  from ..errors import ParameterError, ParseError, PropertyError
23
23
 
24
24
  if TYPE_CHECKING: # pragma: no cover
25
+ from .model_property import ModelProperty
25
26
  from .property import Property
26
27
  else:
28
+ ModelProperty = "ModelProperty"
27
29
  Property = "Property"
28
30
 
29
31
 
@@ -77,6 +79,7 @@ class Schemas:
77
79
  classes_by_reference: Dict[ReferencePath, Property] = field(factory=dict)
78
80
  dependencies: Dict[ReferencePath, Set[Union[ReferencePath, ClassName]]] = field(factory=dict)
79
81
  classes_by_name: Dict[ClassName, Property] = field(factory=dict)
82
+ models_to_process: List[ModelProperty] = field(factory=list)
80
83
  errors: List[ParseError] = field(factory=list)
81
84
 
82
85
  def add_dependencies(self, ref_path: ReferencePath, roots: Set[Union[ReferencePath, ClassName]]) -> None:
@@ -21,8 +21,6 @@ class StringProperty(PropertyProtocol):
21
21
  python_name: PythonIdentifier
22
22
  description: str | None
23
23
  example: str | None
24
- max_length: int | None = None
25
- pattern: str | None = None
26
24
  _type_string: ClassVar[str] = "str"
27
25
  _json_type_string: ClassVar[str] = "str"
28
26
  _allowed_locations: ClassVar[set[oai.ParameterLocation]] = {
@@ -41,7 +39,6 @@ class StringProperty(PropertyProtocol):
41
39
  python_name: PythonIdentifier,
42
40
  description: str | None,
43
41
  example: str | None,
44
- pattern: str | None = None,
45
42
  ) -> StringProperty | PropertyError:
46
43
  checked_default = cls.convert_value(default)
47
44
  return cls(
@@ -51,7 +48,6 @@ class StringProperty(PropertyProtocol):
51
48
  python_name=python_name,
52
49
  description=description,
53
50
  example=example,
54
- pattern=pattern,
55
51
  )
56
52
 
57
53
  @classmethod
@@ -69,4 +65,4 @@ class StringProperty(PropertyProtocol):
69
65
  return value
70
66
  if not isinstance(value, str):
71
67
  value = str(value)
72
- return Value(repr(utils.remove_string_escapes(value)))
68
+ return Value(python_code=repr(utils.remove_string_escapes(value)), raw_value=value)
@@ -1,6 +1,6 @@
1
1
  from typing import Any, Dict, List, Optional, Union
2
2
 
3
- from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, model_validator
3
+ from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, model_validator
4
4
 
5
5
  from ..data_type import DataType
6
6
  from .discriminator import Discriminator
@@ -23,9 +23,9 @@ class Schema(BaseModel):
23
23
  title: Optional[str] = None
24
24
  multipleOf: Optional[float] = Field(default=None, gt=0.0)
25
25
  maximum: Optional[float] = None
26
- exclusiveMaximum: Optional[bool] = None
26
+ exclusiveMaximum: Optional[Union[bool, float]] = None
27
27
  minimum: Optional[float] = None
28
- exclusiveMinimum: Optional[bool] = None
28
+ exclusiveMinimum: Optional[Union[bool, float]] = None
29
29
  maxLength: Optional[int] = Field(default=None, ge=0)
30
30
  minLength: Optional[int] = Field(default=None, ge=0)
31
31
  pattern: Optional[str] = None
@@ -36,7 +36,7 @@ class Schema(BaseModel):
36
36
  minProperties: Optional[int] = Field(default=None, ge=0)
37
37
  required: Optional[List[str]] = Field(default=None, min_length=1)
38
38
  enum: Union[None, List[Any]] = Field(default=None, min_length=1)
39
- const: Union[None, StrictStr, StrictInt] = None
39
+ const: Union[None, StrictStr, StrictInt, StrictFloat, StrictBool] = None
40
40
  type: Union[DataType, List[DataType], None] = Field(default=None)
41
41
  allOf: List[Union[Reference, "Schema"]] = Field(default_factory=list)
42
42
  oneOf: List[Union[Reference, "Schema"]] = Field(default_factory=list)
@@ -160,6 +160,33 @@ class Schema(BaseModel):
160
160
  },
161
161
  )
162
162
 
163
+ @model_validator(mode="after")
164
+ def handle_exclusive_min_max(self) -> "Schema":
165
+ """
166
+ Convert exclusiveMinimum/exclusiveMaximum between OpenAPI v3.0 (bool) and v3.1 (numeric).
167
+ """
168
+ # Handle exclusiveMinimum
169
+ if isinstance(self.exclusiveMinimum, bool) and self.minimum is not None:
170
+ if self.exclusiveMinimum:
171
+ self.exclusiveMinimum = self.minimum
172
+ self.minimum = None
173
+ else:
174
+ self.exclusiveMinimum = None
175
+ elif isinstance(self.exclusiveMinimum, float):
176
+ self.minimum = None
177
+
178
+ # Handle exclusiveMaximum
179
+ if isinstance(self.exclusiveMaximum, bool) and self.maximum is not None:
180
+ if self.exclusiveMaximum:
181
+ self.exclusiveMaximum = self.maximum
182
+ self.maximum = None
183
+ else:
184
+ self.exclusiveMaximum = None
185
+ elif isinstance(self.exclusiveMaximum, float):
186
+ self.maximum = None
187
+
188
+ return self
189
+
163
190
  @model_validator(mode="after")
164
191
  def handle_nullable(self) -> "Schema":
165
192
  """Convert the old 3.0 `nullable` property into the new 3.1 style"""
@@ -138,6 +138,7 @@ return field_dict
138
138
  {% for lazy_import in model.lazy_imports %}
139
139
  {{ lazy_import }}
140
140
  {% endfor %}
141
+ {% if (model.required_properties or model.optional_properties or model.additional_properties) %}
141
142
  d = src_dict.copy()
142
143
  {% for property in model.required_properties + model.optional_properties %}
143
144
  {% if property.required %}
@@ -153,6 +154,7 @@ return field_dict
153
154
  {% endif %}
154
155
 
155
156
  {% endfor %}
157
+ {% endif %}
156
158
  {{ module_name }} = cls(
157
159
  {% for property in model.required_properties + model.optional_properties %}
158
160
  {{ property.python_name }}={{ property.python_name }},
@@ -0,0 +1,5 @@
1
+ {% macro construct(property, source) %}
2
+ {{ property.python_name }} = cast({{ property.get_type_string() }} , {{ source }})
3
+ if {{ property.python_name }} != {{ property.value.python_code }}{% if not property.required %}and not isinstance({{ property.python_name }}, Unset){% endif %}:
4
+ raise ValueError(f"{{ property.name }} must match const {{ property.value.python_code }}, got '{{'{' + property.python_name + '}' }}'")
5
+ {%- endmacro %}
@@ -18,7 +18,7 @@ dependencies = [
18
18
  "typing-extensions>=4.8.0,<5.0.0",
19
19
  ]
20
20
  name = "openapi-python-client"
21
- version = "0.21.3"
21
+ version = "0.21.5"
22
22
  description = "Generate modern Python clients from OpenAPI"
23
23
  keywords = [
24
24
  "OpenAPI",
@@ -63,9 +63,10 @@ ignore = ["E501", "PLR0913"]
63
63
 
64
64
  [tool.ruff.lint.per-file-ignores]
65
65
  "openapi_python_client/cli.py" = ["B008"]
66
+ "tests/*" = ["PLR2004"]
66
67
 
67
68
  [tool.coverage.run]
68
- omit = ["openapi_python_client/templates/*"]
69
+ omit = ["openapi_python_client/__main__.py", "openapi_python_client/templates/*"]
69
70
 
70
71
  [tool.mypy]
71
72
  plugins = ["pydantic.mypy"]
@@ -129,7 +130,7 @@ composite = ["test --cov openapi_python_client tests --cov-report=term-missing"]
129
130
 
130
131
  [tool.pdm.scripts.regen_integration]
131
132
  shell = """
132
- openapi-python-client update --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json --config integration-tests/config.yaml --meta pdm \
133
+ openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json --config integration-tests/config.yaml --meta none --output-path integration-tests/integration_tests \
133
134
  """
134
135
 
135
136
  [build-system]
@@ -1,5 +0,0 @@
1
- {% macro construct(property, source) %}
2
- {{ property.python_name }} = cast({{ property.get_type_string() }} , {{ source }})
3
- if {{ property.python_name }} != {{ property.value }}{% if not property.required %}and not isinstance({{ property.python_name }}, Unset){% endif %}:
4
- raise ValueError(f"{{ property.name }} must match const {{ property.value }}, got '{{'{' + property.python_name + '}' }}'")
5
- {%- endmacro %}