openapi-python-client 0.21.4__tar.gz → 0.21.6__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 (111) hide show
  1. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/PKG-INFO +14 -2
  2. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/README.md +11 -0
  3. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/__init__.py +5 -1
  4. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/config.py +3 -0
  5. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/openapi.py +7 -4
  6. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/__init__.py +23 -3
  7. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/any.py +6 -2
  8. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/boolean.py +3 -3
  9. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/const.py +5 -7
  10. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/date.py +1 -1
  11. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/datetime.py +1 -1
  12. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/enum_property.py +8 -5
  13. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/float.py +3 -3
  14. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/int.py +10 -6
  15. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/list_property.py +19 -3
  16. openapi_python_client-0.21.6/openapi_python_client/parser/properties/literal_enum_property.py +191 -0
  17. openapi_python_client-0.21.6/openapi_python_client/parser/properties/merge_properties.py +198 -0
  18. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/model_property.py +14 -67
  19. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/none.py +1 -1
  20. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/property.py +4 -0
  21. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/protocol.py +12 -4
  22. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/string.py +1 -5
  23. openapi_python_client-0.21.6/openapi_python_client/parser/properties/uuid.py +80 -0
  24. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/schema.py +1 -0
  25. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/client.py.jinja +1 -1
  26. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/endpoint_module.py.jinja +2 -2
  27. openapi_python_client-0.21.6/openapi_python_client/templates/literal_enum.py.jinja +10 -0
  28. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/model.py.jinja +3 -1
  29. openapi_python_client-0.21.6/openapi_python_client/templates/property_templates/const_property.py.jinja +5 -0
  30. openapi_python_client-0.21.6/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +38 -0
  31. openapi_python_client-0.21.6/openapi_python_client/templates/property_templates/uuid_property.py.jinja +38 -0
  32. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/pyproject.toml +6 -5
  33. openapi_python_client-0.21.4/openapi_python_client/templates/property_templates/const_property.py.jinja +0 -5
  34. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/.gitignore +0 -0
  35. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/LICENSE +0 -0
  36. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/__main__.py +0 -0
  37. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/cli.py +0 -0
  38. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/__init__.py +0 -0
  39. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/bodies.py +0 -0
  40. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/errors.py +0 -0
  41. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/file.py +0 -0
  42. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/schemas.py +0 -0
  43. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/properties/union.py +0 -0
  44. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/parser/responses.py +0 -0
  45. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/py.typed +0 -0
  46. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/3.0.3.md +0 -0
  47. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/3.1.0.md +0 -0
  48. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/__init__.py +0 -0
  49. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/data_type.py +0 -0
  50. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/LICENSE +0 -0
  51. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/README.md +0 -0
  52. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/__init__.py +0 -0
  53. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/callback.py +0 -0
  54. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/components.py +0 -0
  55. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/contact.py +0 -0
  56. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +0 -0
  57. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/encoding.py +0 -0
  58. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/example.py +0 -0
  59. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +0 -0
  60. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/header.py +0 -0
  61. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/info.py +0 -0
  62. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/license.py +0 -0
  63. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/link.py +0 -0
  64. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/media_type.py +0 -0
  65. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +0 -0
  66. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py +0 -0
  67. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/open_api.py +0 -0
  68. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/operation.py +0 -0
  69. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/parameter.py +0 -0
  70. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/path_item.py +0 -0
  71. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/paths.py +0 -0
  72. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/reference.py +0 -0
  73. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/request_body.py +0 -0
  74. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/response.py +0 -0
  75. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/responses.py +0 -0
  76. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +0 -0
  77. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +0 -0
  78. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/server.py +0 -0
  79. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +0 -0
  80. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/tag.py +0 -0
  81. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/openapi_schema_pydantic/xml.py +0 -0
  82. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/schema/parameter_location.py +0 -0
  83. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/.gitignore.jinja +0 -0
  84. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/README.md.jinja +0 -0
  85. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/api_init.py.jinja +0 -0
  86. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/endpoint_init.py.jinja +0 -0
  87. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/endpoint_macros.py.jinja +0 -0
  88. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/errors.py.jinja +0 -0
  89. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/helpers.jinja +0 -0
  90. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/int_enum.py.jinja +0 -0
  91. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/models_init.py.jinja +0 -0
  92. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/package_init.py.jinja +0 -0
  93. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/any_property.py.jinja +0 -0
  94. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/boolean_property.py.jinja +0 -0
  95. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/date_property.py.jinja +0 -0
  96. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/datetime_property.py.jinja +0 -0
  97. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/enum_property.py.jinja +0 -0
  98. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/file_property.py.jinja +0 -0
  99. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/float_property.py.jinja +0 -0
  100. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/helpers.jinja +0 -0
  101. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/int_property.py.jinja +0 -0
  102. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/list_property.py.jinja +0 -0
  103. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/model_property.py.jinja +0 -0
  104. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/property_macros.py.jinja +0 -0
  105. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/property_templates/union_property.py.jinja +0 -0
  106. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/pyproject.toml.jinja +0 -0
  107. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/pyproject_ruff.toml.jinja +0 -0
  108. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/setup.py.jinja +0 -0
  109. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/str_enum.py.jinja +0 -0
  110. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/openapi_python_client/templates/types.py.jinja +0 -0
  111. {openapi_python_client-0.21.4 → openapi_python_client-0.21.6}/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.6
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>
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
19
20
  Classifier: Topic :: Software Development :: Code Generators
20
21
  Classifier: Typing :: Typed
21
22
  Requires-Python: <4.0,>=3.8.1
@@ -26,7 +27,7 @@ Requires-Dist: jinja2<4.0.0,>=3.0.0
26
27
  Requires-Dist: pydantic<3.0.0,>=2.1.1
27
28
  Requires-Dist: python-dateutil<3.0.0,>=2.8.1
28
29
  Requires-Dist: ruamel-yaml<0.19.0,>=0.18.6
29
- Requires-Dist: ruff<0.7,>=0.2
30
+ Requires-Dist: ruff<0.8,>=0.2
30
31
  Requires-Dist: shellingham<2.0.0,>=1.3.2
31
32
  Requires-Dist: typer<0.13,>0.6
32
33
  Requires-Dist: typing-extensions<5.0.0,>=4.8.0
@@ -131,6 +132,17 @@ class_overrides:
131
132
 
132
133
  The easiest way to find what needs to be overridden is probably to generate your client and go look at everything in the `models` folder.
133
134
 
135
+ ### literal_enums
136
+
137
+ By default, `openapi-python-client` generates classes inheriting for `Enum` for enums. It can instead use `Literal`
138
+ values for enums by setting this to `true`:
139
+
140
+ ```yaml
141
+ literal_enums: true
142
+ ```
143
+
144
+ This is especially useful if enum values, when transformed to their Python names, end up conflicting due to case sensitivity or special symbols.
145
+
134
146
  ### project_name_override and package_name_override
135
147
 
136
148
  Used to change the name of generated client library project/package. If the project name is changed but an override for the package name
@@ -97,6 +97,17 @@ class_overrides:
97
97
 
98
98
  The easiest way to find what needs to be overridden is probably to generate your client and go look at everything in the `models` folder.
99
99
 
100
+ ### literal_enums
101
+
102
+ By default, `openapi-python-client` generates classes inheriting for `Enum` for enums. It can instead use `Literal`
103
+ values for enums by setting this to `true`:
104
+
105
+ ```yaml
106
+ literal_enums: true
107
+ ```
108
+
109
+ This is especially useful if enum values, when transformed to their Python names, end up conflicting due to case sensitivity or special symbols.
110
+
100
111
  ### project_name_override and package_name_override
101
112
 
102
113
  Used to change the name of generated client library project/package. If the project name is changed but an override for the package name
@@ -20,6 +20,7 @@ from openapi_python_client import utils
20
20
  from .config import Config, MetaType
21
21
  from .parser import GeneratorData, import_string_from_class
22
22
  from .parser.errors import ErrorLevel, GeneratorError
23
+ from .parser.properties import LiteralEnumProperty
23
24
 
24
25
  __version__ = version(__package__)
25
26
 
@@ -227,9 +228,12 @@ class Project:
227
228
  # Generate enums
228
229
  str_enum_template = self.env.get_template("str_enum.py.jinja")
229
230
  int_enum_template = self.env.get_template("int_enum.py.jinja")
231
+ literal_enum_template = self.env.get_template("literal_enum.py.jinja")
230
232
  for enum in self.openapi.enums:
231
233
  module_path = models_dir / f"{enum.class_info.module_name}.py"
232
- if enum.value_type is int:
234
+ if isinstance(enum, LiteralEnumProperty):
235
+ module_path.write_text(literal_enum_template.render(enum=enum), encoding=self.config.file_encoding)
236
+ elif enum.value_type is int:
233
237
  module_path.write_text(int_enum_template.render(enum=enum), encoding=self.config.file_encoding)
234
238
  else:
235
239
  module_path.write_text(str_enum_template.render(enum=enum), encoding=self.config.file_encoding)
@@ -43,6 +43,7 @@ class ConfigFile(BaseModel):
43
43
  post_hooks: Optional[List[str]] = None
44
44
  field_prefix: str = "field_"
45
45
  http_timeout: int = 5
46
+ literal_enums: bool = False
46
47
 
47
48
  @staticmethod
48
49
  def load_from_path(path: Path) -> "ConfigFile":
@@ -70,6 +71,7 @@ class Config:
70
71
  post_hooks: List[str]
71
72
  field_prefix: str
72
73
  http_timeout: int
74
+ literal_enums: bool
73
75
  document_source: Union[Path, str]
74
76
  file_encoding: str
75
77
  content_type_overrides: Dict[str, str]
@@ -109,6 +111,7 @@ class Config:
109
111
  post_hooks=post_hooks,
110
112
  field_prefix=config_file.field_prefix,
111
113
  http_timeout=config_file.http_timeout,
114
+ literal_enums=config_file.literal_enums,
112
115
  document_source=document_source,
113
116
  file_encoding=file_encoding,
114
117
  overwrite=overwrite,
@@ -15,6 +15,7 @@ from .errors import GeneratorError, ParseError, PropertyError
15
15
  from .properties import (
16
16
  Class,
17
17
  EnumProperty,
18
+ LiteralEnumProperty,
18
19
  ModelProperty,
19
20
  Parameters,
20
21
  Property,
@@ -155,7 +156,7 @@ class Endpoint:
155
156
  ParseError(
156
157
  detail=(
157
158
  f"Invalid response status code {code} (not a valid HTTP "
158
- f"status code), response will be ommitted from generated "
159
+ f"status code), response will be omitted from generated "
159
160
  f"client"
160
161
  )
161
162
  )
@@ -175,7 +176,7 @@ class Endpoint:
175
176
  ParseError(
176
177
  detail=(
177
178
  f"Cannot parse response for status code {status_code}{detail_suffix}, "
178
- f"response will be ommitted from generated client"
179
+ f"response will be omitted from generated client"
179
180
  ),
180
181
  data=response.data,
181
182
  )
@@ -488,7 +489,7 @@ class GeneratorData:
488
489
  models: Iterator[ModelProperty]
489
490
  errors: List[ParseError]
490
491
  endpoint_collections_by_tag: Dict[utils.PythonIdentifier, EndpointCollection]
491
- enums: Iterator[EnumProperty]
492
+ enums: Iterator[Union[EnumProperty, LiteralEnumProperty]]
492
493
 
493
494
  @staticmethod
494
495
  def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData", GeneratorError]:
@@ -517,7 +518,9 @@ class GeneratorData:
517
518
  data=openapi.paths, schemas=schemas, parameters=parameters, request_bodies=request_bodies, config=config
518
519
  )
519
520
 
520
- enums = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, EnumProperty))
521
+ enums = (
522
+ prop for prop in schemas.classes_by_name.values() if isinstance(prop, (EnumProperty, LiteralEnumProperty))
523
+ )
521
524
  models = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, ModelProperty))
522
525
 
523
526
  return GeneratorData(
@@ -4,6 +4,7 @@ __all__ = [
4
4
  "AnyProperty",
5
5
  "Class",
6
6
  "EnumProperty",
7
+ "LiteralEnumProperty",
7
8
  "ModelProperty",
8
9
  "Parameters",
9
10
  "Property",
@@ -30,6 +31,7 @@ from .file import FileProperty
30
31
  from .float import FloatProperty
31
32
  from .int import IntProperty
32
33
  from .list_property import ListProperty
34
+ from .literal_enum_property import LiteralEnumProperty
33
35
  from .model_property import ModelProperty, process_model
34
36
  from .none import NoneProperty
35
37
  from .property import Property
@@ -44,11 +46,12 @@ from .schemas import (
44
46
  )
45
47
  from .string import StringProperty
46
48
  from .union import UnionProperty
49
+ from .uuid import UuidProperty
47
50
 
48
51
 
49
52
  def _string_based_property(
50
53
  name: str, required: bool, data: oai.Schema, config: Config
51
- ) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | PropertyError:
54
+ ) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | UuidProperty | PropertyError:
52
55
  """Construct a Property from the type "string" """
53
56
  string_format = data.schema_format
54
57
  python_name = utils.PythonIdentifier(value=name, prefix=config.field_prefix)
@@ -79,11 +82,19 @@ def _string_based_property(
79
82
  description=data.description,
80
83
  example=data.example,
81
84
  )
85
+ if string_format == "uuid":
86
+ return UuidProperty.build(
87
+ name=name,
88
+ required=required,
89
+ default=data.default,
90
+ python_name=python_name,
91
+ description=data.description,
92
+ example=data.example,
93
+ )
82
94
  return StringProperty.build(
83
95
  name=name,
84
96
  default=data.default,
85
97
  required=required,
86
- pattern=data.pattern,
87
98
  python_name=python_name,
88
99
  description=data.description,
89
100
  example=data.example,
@@ -185,6 +196,15 @@ def property_from_data( # noqa: PLR0911, PLR0912
185
196
  schemas,
186
197
  )
187
198
  if data.enum:
199
+ if config.literal_enums:
200
+ return LiteralEnumProperty.build(
201
+ data=data,
202
+ name=name,
203
+ required=required,
204
+ schemas=schemas,
205
+ parent_name=parent_name,
206
+ config=config,
207
+ )
188
208
  return EnumProperty.build(
189
209
  data=data,
190
210
  name=name,
@@ -281,7 +301,7 @@ def property_from_data( # noqa: PLR0911, PLR0912
281
301
  AnyProperty.build(
282
302
  name=name,
283
303
  required=required,
284
- default=None,
304
+ default=data.default,
285
305
  python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
286
306
  description=data.description,
287
307
  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
 
@@ -121,7 +121,7 @@ class EnumProperty(PropertyProtocol):
121
121
  if parent_name:
122
122
  class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}"
123
123
  class_info = Class.from_string(string=class_name, config=config)
124
- values = EnumProperty.values_from_list(value_list)
124
+ values = EnumProperty.values_from_list(value_list, class_info)
125
125
 
126
126
  if class_info.name in schemas.classes_by_name:
127
127
  existing = schemas.classes_by_name[class_info.name]
@@ -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}")
@@ -183,7 +183,7 @@ class EnumProperty(PropertyProtocol):
183
183
  return imports
184
184
 
185
185
  @staticmethod
186
- def values_from_list(values: list[str] | list[int]) -> dict[str, ValueType]:
186
+ def values_from_list(values: list[str] | list[int], class_info: Class) -> dict[str, ValueType]:
187
187
  """Convert a list of values into dict of {name: value}, where value can sometimes be None"""
188
188
  output: dict[str, ValueType] = {}
189
189
 
@@ -200,7 +200,10 @@ class EnumProperty(PropertyProtocol):
200
200
  else:
201
201
  key = f"VALUE_{i}"
202
202
  if key in output:
203
- raise ValueError(f"Duplicate key {key} in Enum")
203
+ raise ValueError(
204
+ f"Duplicate key {key} in enum {class_info.module_name}.{class_info.name}; "
205
+ f"consider setting literal_enums in your config"
206
+ )
204
207
  sanitized_key = utils.snake_case(key).upper()
205
208
  output[sanitized_key] = utils.remove_string_escapes(value)
206
209
  return output
@@ -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}")
@@ -58,12 +58,28 @@ class ListProperty(PropertyProtocol):
58
58
  """
59
59
  from . import property_from_data
60
60
 
61
- if data.items is None:
62
- return PropertyError(data=data, detail="type array must have items defined"), schemas
61
+ if data.items is None and not data.prefixItems:
62
+ return (
63
+ PropertyError(
64
+ data=data,
65
+ detail="type array must have items or prefixItems defined",
66
+ ),
67
+ schemas,
68
+ )
69
+
70
+ items = data.prefixItems or []
71
+ if data.items:
72
+ items.append(data.items)
73
+
74
+ if len(items) == 1:
75
+ inner_schema = items[0]
76
+ else:
77
+ inner_schema = oai.Schema(anyOf=items)
78
+
63
79
  inner_prop, schemas = property_from_data(
64
80
  name=f"{name}_item",
65
81
  required=True,
66
- data=data.items,
82
+ data=inner_schema,
67
83
  schemas=schemas,
68
84
  parent_name=parent_name,
69
85
  config=config,
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ["LiteralEnumProperty"]
4
+
5
+ from typing import Any, ClassVar, List, Union, cast
6
+
7
+ from attr import evolve
8
+ from attrs import define
9
+
10
+ from ... import Config, utils
11
+ from ... import schema as oai
12
+ from ...schema import DataType
13
+ from ..errors import PropertyError
14
+ from .none import NoneProperty
15
+ from .protocol import PropertyProtocol, Value
16
+ from .schemas import Class, Schemas
17
+ from .union import UnionProperty
18
+
19
+ ValueType = Union[str, int]
20
+
21
+
22
+ @define
23
+ class LiteralEnumProperty(PropertyProtocol):
24
+ """A property that should use a literal enum"""
25
+
26
+ name: str
27
+ required: bool
28
+ default: Value | None
29
+ python_name: utils.PythonIdentifier
30
+ description: str | None
31
+ example: str | None
32
+ values: set[ValueType]
33
+ class_info: Class
34
+ value_type: type[ValueType]
35
+
36
+ template: ClassVar[str] = "literal_enum_property.py.jinja"
37
+
38
+ _allowed_locations: ClassVar[set[oai.ParameterLocation]] = {
39
+ oai.ParameterLocation.QUERY,
40
+ oai.ParameterLocation.PATH,
41
+ oai.ParameterLocation.COOKIE,
42
+ oai.ParameterLocation.HEADER,
43
+ }
44
+
45
+ @classmethod
46
+ def build( # noqa: PLR0911
47
+ cls,
48
+ *,
49
+ data: oai.Schema,
50
+ name: str,
51
+ required: bool,
52
+ schemas: Schemas,
53
+ parent_name: str,
54
+ config: Config,
55
+ ) -> tuple[LiteralEnumProperty | NoneProperty | UnionProperty | PropertyError, Schemas]:
56
+ """
57
+ Create a LiteralEnumProperty from schema data.
58
+
59
+ Args:
60
+ data: The OpenAPI Schema which defines this enum.
61
+ name: The name to use for variables which receive this Enum's value (e.g. model property name)
62
+ required: Whether or not this Property is required in the calling context
63
+ schemas: The Schemas which have been defined so far (used to prevent naming collisions)
64
+ parent_name: The context in which this LiteralEnumProperty is defined, used to create more specific class names.
65
+ config: The global config for this run of the generator
66
+
67
+ Returns:
68
+ A tuple containing either the created property or a PropertyError AND update schemas.
69
+ """
70
+
71
+ enum = data.enum or [] # The outer function checks for this, but mypy doesn't know that
72
+
73
+ # OpenAPI allows for null as an enum value, but it doesn't make sense with how enums are constructed in Python.
74
+ # So instead, if null is a possible value, make the property nullable.
75
+ # Mypy is not smart enough to know that the type is right though
76
+ unchecked_value_list = [value for value in enum if value is not None] # type: ignore
77
+
78
+ # It's legal to have an enum that only contains null as a value, we don't bother constructing an enum for that
79
+ if len(unchecked_value_list) == 0:
80
+ return (
81
+ NoneProperty.build(
82
+ name=name,
83
+ required=required,
84
+ default="None",
85
+ python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
86
+ description=None,
87
+ example=None,
88
+ ),
89
+ schemas,
90
+ )
91
+
92
+ value_types = {type(value) for value in unchecked_value_list}
93
+ if len(value_types) > 1:
94
+ return PropertyError(
95
+ header="Enum values must all be the same type", detail=f"Got {value_types}", data=data
96
+ ), schemas
97
+ value_type = next(iter(value_types))
98
+ if value_type not in (str, int):
99
+ return PropertyError(header=f"Unsupported enum type {value_type}", data=data), schemas
100
+ value_list = cast(
101
+ Union[List[int], List[str]], unchecked_value_list
102
+ ) # We checked this with all the value_types stuff
103
+
104
+ if len(value_list) < len(enum): # Only one of the values was None, that becomes a union
105
+ data.oneOf = [
106
+ oai.Schema(type=DataType.NULL),
107
+ data.model_copy(update={"enum": value_list, "default": data.default}),
108
+ ]
109
+ data.enum = None
110
+ return UnionProperty.build(
111
+ data=data,
112
+ name=name,
113
+ required=required,
114
+ schemas=schemas,
115
+ parent_name=parent_name,
116
+ config=config,
117
+ )
118
+
119
+ class_name = data.title or name
120
+ if parent_name:
121
+ class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}"
122
+ class_info = Class.from_string(string=class_name, config=config)
123
+ values: set[str | int] = set(value_list)
124
+
125
+ if class_info.name in schemas.classes_by_name:
126
+ existing = schemas.classes_by_name[class_info.name]
127
+ if not isinstance(existing, LiteralEnumProperty) or values != existing.values:
128
+ return (
129
+ PropertyError(
130
+ detail=f"Found conflicting enums named {class_info.name} with incompatible values.", data=data
131
+ ),
132
+ schemas,
133
+ )
134
+
135
+ prop = LiteralEnumProperty(
136
+ name=name,
137
+ required=required,
138
+ class_info=class_info,
139
+ values=values,
140
+ value_type=value_type,
141
+ default=None,
142
+ python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
143
+ description=data.description,
144
+ example=data.example,
145
+ )
146
+ checked_default = prop.convert_value(data.default)
147
+ if isinstance(checked_default, PropertyError):
148
+ checked_default.data = data
149
+ return checked_default, schemas
150
+ prop = evolve(prop, default=checked_default)
151
+
152
+ schemas = evolve(schemas, classes_by_name={**schemas.classes_by_name, class_info.name: prop})
153
+ return prop, schemas
154
+
155
+ def convert_value(self, value: Any) -> Value | PropertyError | None:
156
+ if value is None or isinstance(value, Value):
157
+ return value
158
+ if isinstance(value, self.value_type):
159
+ if value in self.values:
160
+ return Value(python_code=repr(value), raw_value=value)
161
+ else:
162
+ return PropertyError(detail=f"Value {value} is not valid for enum {self.name}")
163
+ return PropertyError(detail=f"Cannot convert {value} to enum {self.name} of type {self.value_type}")
164
+
165
+ def get_base_type_string(self, *, quoted: bool = False) -> str:
166
+ return self.class_info.name
167
+
168
+ def get_base_json_type_string(self, *, quoted: bool = False) -> str:
169
+ return self.value_type.__name__
170
+
171
+ def get_instance_type_string(self) -> str:
172
+ return self.value_type.__name__
173
+
174
+ def get_imports(self, *, prefix: str) -> set[str]:
175
+ """
176
+ Get a set of import strings that should be included when this property is used somewhere
177
+
178
+ Args:
179
+ prefix: A prefix to put before any relative (local) module names. This should be the number of . to get
180
+ back to the root of the generated client.
181
+ """
182
+ imports = super().get_imports(prefix=prefix)
183
+ imports.add("from typing import cast")
184
+ imports.add(f"from {prefix}models.{self.class_info.module_name} import {self.class_info.name}")
185
+ imports.add(
186
+ f"from {prefix}models.{self.class_info.module_name} import check_{self.get_class_name_snake_case()}"
187
+ )
188
+ return imports
189
+
190
+ def get_class_name_snake_case(self) -> str:
191
+ return utils.snake_case(self.class_info.name)