openapi-python-client 0.21.4__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.4 → openapi_python_client-0.21.5}/PKG-INFO +1 -1
  2. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/openapi.py +2 -2
  3. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/__init__.py +1 -2
  4. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/any.py +6 -2
  5. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/boolean.py +3 -3
  6. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/const.py +5 -7
  7. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/date.py +1 -1
  8. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/datetime.py +1 -1
  9. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/enum_property.py +2 -2
  10. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/float.py +3 -3
  11. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/int.py +10 -6
  12. openapi_python_client-0.21.5/openapi_python_client/parser/properties/merge_properties.py +168 -0
  13. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/model_property.py +14 -67
  14. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/none.py +1 -1
  15. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/protocol.py +12 -4
  16. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/string.py +1 -5
  17. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/model.py.jinja +2 -0
  18. openapi_python_client-0.21.5/openapi_python_client/templates/property_templates/const_property.py.jinja +5 -0
  19. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/pyproject.toml +2 -2
  20. openapi_python_client-0.21.4/openapi_python_client/templates/property_templates/const_property.py.jinja +0 -5
  21. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/.gitignore +0 -0
  22. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/LICENSE +0 -0
  23. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/README.md +0 -0
  24. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/__init__.py +0 -0
  25. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/__main__.py +0 -0
  26. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/cli.py +0 -0
  27. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/config.py +0 -0
  28. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/__init__.py +0 -0
  29. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/bodies.py +0 -0
  30. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/errors.py +0 -0
  31. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/file.py +0 -0
  32. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/list_property.py +0 -0
  33. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/property.py +0 -0
  34. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/schemas.py +0 -0
  35. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/properties/union.py +0 -0
  36. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/parser/responses.py +0 -0
  37. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/py.typed +0 -0
  38. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/3.0.3.md +0 -0
  39. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/3.1.0.md +0 -0
  40. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/__init__.py +0 -0
  41. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/data_type.py +0 -0
  42. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/LICENSE +0 -0
  43. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/README.md +0 -0
  44. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/__init__.py +0 -0
  45. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/callback.py +0 -0
  46. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/components.py +0 -0
  47. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/contact.py +0 -0
  48. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +0 -0
  49. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/encoding.py +0 -0
  50. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/example.py +0 -0
  51. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +0 -0
  52. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/header.py +0 -0
  53. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/info.py +0 -0
  54. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/license.py +0 -0
  55. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/link.py +0 -0
  56. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/media_type.py +0 -0
  57. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +0 -0
  58. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py +0 -0
  59. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/open_api.py +0 -0
  60. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/operation.py +0 -0
  61. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/parameter.py +0 -0
  62. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/path_item.py +0 -0
  63. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/paths.py +0 -0
  64. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/reference.py +0 -0
  65. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/request_body.py +0 -0
  66. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/response.py +0 -0
  67. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/responses.py +0 -0
  68. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/schema.py +0 -0
  69. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +0 -0
  70. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +0 -0
  71. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/server.py +0 -0
  72. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +0 -0
  73. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/tag.py +0 -0
  74. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/openapi_schema_pydantic/xml.py +0 -0
  75. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/schema/parameter_location.py +0 -0
  76. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/.gitignore.jinja +0 -0
  77. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/README.md.jinja +0 -0
  78. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/api_init.py.jinja +0 -0
  79. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/client.py.jinja +0 -0
  80. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/endpoint_init.py.jinja +0 -0
  81. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/endpoint_macros.py.jinja +0 -0
  82. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/endpoint_module.py.jinja +0 -0
  83. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/errors.py.jinja +0 -0
  84. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/helpers.jinja +0 -0
  85. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/int_enum.py.jinja +0 -0
  86. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/models_init.py.jinja +0 -0
  87. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/package_init.py.jinja +0 -0
  88. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/any_property.py.jinja +0 -0
  89. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/boolean_property.py.jinja +0 -0
  90. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/date_property.py.jinja +0 -0
  91. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/datetime_property.py.jinja +0 -0
  92. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/enum_property.py.jinja +0 -0
  93. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/file_property.py.jinja +0 -0
  94. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/float_property.py.jinja +0 -0
  95. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/helpers.jinja +0 -0
  96. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/int_property.py.jinja +0 -0
  97. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/list_property.py.jinja +0 -0
  98. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/model_property.py.jinja +0 -0
  99. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/property_macros.py.jinja +0 -0
  100. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/property_templates/union_property.py.jinja +0 -0
  101. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/pyproject.toml.jinja +0 -0
  102. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/pyproject_ruff.toml.jinja +0 -0
  103. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/setup.py.jinja +0 -0
  104. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/str_enum.py.jinja +0 -0
  105. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/templates/types.py.jinja +0 -0
  106. {openapi_python_client-0.21.4 → openapi_python_client-0.21.5}/openapi_python_client/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openapi-python-client
3
- Version: 0.21.4
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>
@@ -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,
@@ -281,7 +280,7 @@ def property_from_data( # noqa: PLR0911, PLR0912
281
280
  AnyProperty.build(
282
281
  name=name,
283
282
  required=required,
284
- default=None,
283
+ default=data.default,
285
284
  python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
286
285
  description=data.description,
287
286
  example=data.example,
@@ -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}")
@@ -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
@@ -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
 
@@ -220,59 +219,6 @@ class ModelProperty(PropertyProtocol):
220
219
  from .property import Property # noqa: E402
221
220
 
222
221
 
223
- def _values_are_subset(first: EnumProperty, second: EnumProperty) -> bool:
224
- return set(first.values.items()) <= set(second.values.items())
225
-
226
-
227
- def _types_are_subset(first: EnumProperty, second: Property) -> bool:
228
- from . import IntProperty, StringProperty
229
-
230
- if first.value_type is int and isinstance(second, IntProperty):
231
- return True
232
- if first.value_type is str and isinstance(second, StringProperty):
233
- return True
234
- return False
235
-
236
-
237
- def _enum_subset(first: Property, second: Property) -> EnumProperty | None:
238
- """Return the EnumProperty that is the subset of the other, if possible."""
239
-
240
- if isinstance(first, EnumProperty):
241
- if isinstance(second, EnumProperty):
242
- if _values_are_subset(first, second):
243
- return first
244
- if _values_are_subset(second, first):
245
- return second
246
- return None
247
- return first if _types_are_subset(first, second) else None
248
-
249
- if isinstance(second, EnumProperty) and _types_are_subset(second, first):
250
- return second
251
- return None
252
-
253
-
254
- def _merge_properties(first: Property, second: Property) -> Property | PropertyError:
255
- required = first.required or second.required
256
-
257
- err = None
258
-
259
- if first.__class__ == second.__class__:
260
- first = evolve(first, required=required)
261
- second = evolve(second, required=required)
262
- if first == second:
263
- return first
264
- err = PropertyError(header="Cannot merge properties", detail="Properties has conflicting values")
265
-
266
- enum_subset = _enum_subset(first, second)
267
- if enum_subset is not None:
268
- return evolve(enum_subset, required=required)
269
-
270
- return err or PropertyError(
271
- header="Cannot merge properties",
272
- detail=f"{first.__class__}, {second.__class__}Properties have incompatible types",
273
- )
274
-
275
-
276
222
  def _resolve_naming_conflict(first: Property, second: Property, config: Config) -> PropertyError | None:
277
223
  first.set_python_name(first.name, config=config, skip_snake_case=True)
278
224
  second.set_python_name(second.name, config=config, skip_snake_case=True)
@@ -301,6 +247,7 @@ def _process_properties( # noqa: PLR0912, PLR0911
301
247
  roots: set[ReferencePath | utils.ClassName],
302
248
  ) -> _PropertyData | PropertyError:
303
249
  from . import property_from_data
250
+ from .merge_properties import merge_properties
304
251
 
305
252
  properties: dict[str, Property] = {}
306
253
  relative_imports: set[str] = set()
@@ -311,26 +258,26 @@ def _process_properties( # noqa: PLR0912, PLR0911
311
258
  nonlocal properties
312
259
 
313
260
  name_conflict = properties.get(new_prop.name)
314
- merged_prop_or_error = _merge_properties(name_conflict, new_prop) if name_conflict else new_prop
315
- if isinstance(merged_prop_or_error, PropertyError):
316
- merged_prop_or_error.header = (
317
- f"Found conflicting properties named {new_prop.name} when creating {class_name}"
318
- )
319
- 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
320
265
 
321
266
  for other_prop in properties.values():
322
- if other_prop.name == merged_prop_or_error.name:
267
+ if other_prop.name == merged_prop.name:
323
268
  continue # Same property, probably just got merged
324
- if other_prop.python_name != merged_prop_or_error.python_name:
269
+ if other_prop.python_name != merged_prop.python_name:
325
270
  continue
326
- naming_error = _resolve_naming_conflict(merged_prop_or_error, other_prop, config)
271
+ naming_error = _resolve_naming_conflict(merged_prop, other_prop, config)
327
272
  if naming_error is not None:
328
273
  return naming_error
329
274
 
330
- properties[merged_prop_or_error.name] = merged_prop_or_error
275
+ properties[merged_prop.name] = merged_prop
331
276
  return None
332
277
 
333
- 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
+ )
334
281
  for sub_prop in data.allOf:
335
282
  if isinstance(sub_prop, oai.Reference):
336
283
  ref_path = parse_reference_path(sub_prop.ref)
@@ -352,10 +299,10 @@ def _process_properties( # noqa: PLR0912, PLR0911
352
299
  return err
353
300
  schemas.add_dependencies(ref_path=ref_path, roots=roots)
354
301
  else:
355
- unprocessed_props.update(sub_prop.properties or {})
302
+ unprocessed_props.extend(sub_prop.properties.items() if sub_prop.properties else [])
356
303
  required_set.update(sub_prop.required or [])
357
304
 
358
- for key, value in unprocessed_props.items():
305
+ for key, value in unprocessed_props:
359
306
  prop_required = key in required_set
360
307
  prop_or_error: Property | (PropertyError | None)
361
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
@@ -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)
@@ -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.4"
21
+ version = "0.21.5"
22
22
  description = "Generate modern Python clients from OpenAPI"
23
23
  keywords = [
24
24
  "OpenAPI",
@@ -66,7 +66,7 @@ ignore = ["E501", "PLR0913"]
66
66
  "tests/*" = ["PLR2004"]
67
67
 
68
68
  [tool.coverage.run]
69
- omit = ["openapi_python_client/templates/*"]
69
+ omit = ["openapi_python_client/__main__.py", "openapi_python_client/templates/*"]
70
70
 
71
71
  [tool.mypy]
72
72
  plugins = ["pydantic.mypy"]
@@ -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 %}