Skip to content

Commit dd0258c

Browse files
committed
feat: add UUID format for string properties
Used by FastAPI and possibly others
1 parent db6f4f2 commit dd0258c

File tree

7 files changed

+225
-1
lines changed

7 files changed

+225
-1
lines changed

end_to_end_tests/baseline_openapi_3.0.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,8 @@
17141714
"aCamelDateTime",
17151715
"a_date",
17161716
"a_nullable_date",
1717+
"a_uuid",
1718+
"a_nullable_uuid",
17171719
"required_nullable",
17181720
"required_not_nullable",
17191721
"model",
@@ -1784,6 +1786,22 @@
17841786
"type": "string",
17851787
"format": "date"
17861788
},
1789+
"a_uuid": {
1790+
"title": "A Uuid",
1791+
"type": "string",
1792+
"format": "uuid"
1793+
},
1794+
"a_nullable_uuid": {
1795+
"title": "A Nullable Uuid",
1796+
"type": "string",
1797+
"format": "uuid",
1798+
"nullable": true
1799+
},
1800+
"a_not_required_uuid": {
1801+
"title": "A Not Required Uuid",
1802+
"type": "string",
1803+
"format": "uuid"
1804+
},
17871805
"1_leading_digit": {
17881806
"title": "Leading Digit",
17891807
"type": "string"

end_to_end_tests/baseline_openapi_3.1.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,6 +1699,8 @@ info:
16991699
"aCamelDateTime",
17001700
"a_date",
17011701
"a_nullable_date",
1702+
"a_uuid",
1703+
"a_nullable_uuid",
17021704
"required_nullable",
17031705
"required_not_nullable",
17041706
"model",
@@ -1771,6 +1773,28 @@ info:
17711773
"type": "string",
17721774
"format": "date"
17731775
},
1776+
"a_uuid": {
1777+
"title": "A Uuid",
1778+
"type": "string",
1779+
"format": "uuid"
1780+
},
1781+
"a_nullable_uuid": {
1782+
"title": "A Nullable Uuid",
1783+
"anyOf": [
1784+
{
1785+
"type": "string",
1786+
"format": "uuid"
1787+
},
1788+
{
1789+
"type": "null"
1790+
}
1791+
]
1792+
},
1793+
"a_not_required_uuid": {
1794+
"title": "A Not Required Uuid",
1795+
"type": "string",
1796+
"format": "uuid"
1797+
},
17741798
"1_leading_digit": {
17751799
"title": "Leading Digit",
17761800
"type": "string"

end_to_end_tests/golden-record/my_test_api_client/models/a_model.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import uuid
23
from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union, cast
34

45
from attrs import define as _attrs_define
@@ -27,6 +28,8 @@ class AModel:
2728
a_camel_date_time (Union[datetime.date, datetime.datetime]):
2829
a_date (datetime.date):
2930
a_nullable_date (Union[None, datetime.date]):
31+
a_uuid (uuid.UUID):
32+
a_nullable_uuid (Union[None, uuid.UUID]):
3033
required_nullable (Union[None, str]):
3134
required_not_nullable (str):
3235
one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Any]):
@@ -37,6 +40,7 @@ class AModel:
3740
an_optional_allof_enum (Union[Unset, AnAllOfEnum]):
3841
nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]):
3942
a_not_required_date (Union[Unset, datetime.date]):
43+
a_not_required_uuid (Union[Unset, uuid.UUID]):
4044
attr_1_leading_digit (Union[Unset, str]):
4145
attr_leading_underscore (Union[Unset, str]):
4246
not_required_nullable (Union[None, Unset, str]):
@@ -51,6 +55,8 @@ class AModel:
5155
a_camel_date_time: Union[datetime.date, datetime.datetime]
5256
a_date: datetime.date
5357
a_nullable_date: Union[None, datetime.date]
58+
a_uuid: uuid.UUID
59+
a_nullable_uuid: Union[None, uuid.UUID]
5460
required_nullable: Union[None, str]
5561
required_not_nullable: str
5662
one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Any]
@@ -62,6 +68,7 @@ class AModel:
6268
an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET
6369
nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET
6470
a_not_required_date: Union[Unset, datetime.date] = UNSET
71+
a_not_required_uuid: Union[Unset, uuid.UUID] = UNSET
6572
attr_1_leading_digit: Union[Unset, str] = UNSET
6673
attr_leading_underscore: Union[Unset, str] = UNSET
6774
not_required_nullable: Union[None, Unset, str] = UNSET
@@ -93,6 +100,14 @@ def to_dict(self) -> Dict[str, Any]:
93100
else:
94101
a_nullable_date = self.a_nullable_date
95102

103+
a_uuid = str(self.a_uuid)
104+
105+
a_nullable_uuid: Union[None, str]
106+
if isinstance(self.a_nullable_uuid, uuid.UUID):
107+
a_nullable_uuid = str(self.a_nullable_uuid)
108+
else:
109+
a_nullable_uuid = self.a_nullable_uuid
110+
96111
required_nullable: Union[None, str]
97112
required_nullable = self.required_nullable
98113

@@ -143,6 +158,10 @@ def to_dict(self) -> Dict[str, Any]:
143158
if not isinstance(self.a_not_required_date, Unset):
144159
a_not_required_date = self.a_not_required_date.isoformat()
145160

161+
a_not_required_uuid: Union[Unset, str] = UNSET
162+
if not isinstance(self.a_not_required_uuid, Unset):
163+
a_not_required_uuid = str(self.a_not_required_uuid)
164+
146165
attr_1_leading_digit = self.attr_1_leading_digit
147166

148167
attr_leading_underscore = self.attr_leading_underscore
@@ -193,6 +212,8 @@ def to_dict(self) -> Dict[str, Any]:
193212
"aCamelDateTime": a_camel_date_time,
194213
"a_date": a_date,
195214
"a_nullable_date": a_nullable_date,
215+
"a_uuid": a_uuid,
216+
"a_nullable_uuid": a_nullable_uuid,
196217
"required_nullable": required_nullable,
197218
"required_not_nullable": required_not_nullable,
198219
"one_of_models": one_of_models,
@@ -209,6 +230,8 @@ def to_dict(self) -> Dict[str, Any]:
209230
field_dict["nested_list_of_enums"] = nested_list_of_enums
210231
if a_not_required_date is not UNSET:
211232
field_dict["a_not_required_date"] = a_not_required_date
233+
if a_not_required_uuid is not UNSET:
234+
field_dict["a_not_required_uuid"] = a_not_required_uuid
212235
if attr_1_leading_digit is not UNSET:
213236
field_dict["1_leading_digit"] = attr_1_leading_digit
214237
if attr_leading_underscore is not UNSET:
@@ -272,6 +295,23 @@ def _parse_a_nullable_date(data: object) -> Union[None, datetime.date]:
272295

273296
a_nullable_date = _parse_a_nullable_date(d.pop("a_nullable_date"))
274297

298+
a_uuid = uuid.UUID(d.pop("a_uuid"))
299+
300+
def _parse_a_nullable_uuid(data: object) -> Union[None, uuid.UUID]:
301+
if data is None:
302+
return data
303+
try:
304+
if not isinstance(data, str):
305+
raise TypeError()
306+
a_nullable_uuid_type_0 = uuid.UUID(data)
307+
308+
return a_nullable_uuid_type_0
309+
except: # noqa: E722
310+
pass
311+
return cast(Union[None, uuid.UUID], data)
312+
313+
a_nullable_uuid = _parse_a_nullable_uuid(d.pop("a_nullable_uuid"))
314+
275315
def _parse_required_nullable(data: object) -> Union[None, str]:
276316
if data is None:
277317
return data
@@ -370,6 +410,13 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None]
370410
else:
371411
a_not_required_date = isoparse(_a_not_required_date).date()
372412

413+
_a_not_required_uuid = d.pop("a_not_required_uuid", UNSET)
414+
a_not_required_uuid: Union[Unset, uuid.UUID]
415+
if isinstance(_a_not_required_uuid, Unset):
416+
a_not_required_uuid = UNSET
417+
else:
418+
a_not_required_uuid = uuid.UUID(_a_not_required_uuid)
419+
373420
attr_1_leading_digit = d.pop("1_leading_digit", UNSET)
374421

375422
attr_leading_underscore = d.pop("_leading_underscore", UNSET)
@@ -463,6 +510,8 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro
463510
a_camel_date_time=a_camel_date_time,
464511
a_date=a_date,
465512
a_nullable_date=a_nullable_date,
513+
a_uuid=a_uuid,
514+
a_nullable_uuid=a_nullable_uuid,
466515
required_nullable=required_nullable,
467516
required_not_nullable=required_not_nullable,
468517
one_of_models=one_of_models,
@@ -473,6 +522,7 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro
473522
an_optional_allof_enum=an_optional_allof_enum,
474523
nested_list_of_enums=nested_list_of_enums,
475524
a_not_required_date=a_not_required_date,
525+
a_not_required_uuid=a_not_required_uuid,
476526
attr_1_leading_digit=attr_1_leading_digit,
477527
attr_leading_underscore=attr_leading_underscore,
478528
not_required_nullable=not_required_nullable,

openapi_python_client/parser/properties/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@
4444
)
4545
from .string import StringProperty
4646
from .union import UnionProperty
47+
from .uuid import UuidProperty
4748

4849

4950
def _string_based_property(
5051
name: str, required: bool, data: oai.Schema, config: Config
51-
) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | PropertyError:
52+
) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | UuidProperty | PropertyError:
5253
"""Construct a Property from the type "string" """
5354
string_format = data.schema_format
5455
python_name = utils.PythonIdentifier(value=name, prefix=config.field_prefix)
@@ -79,6 +80,15 @@ def _string_based_property(
7980
description=data.description,
8081
example=data.example,
8182
)
83+
if string_format == "uuid":
84+
return UuidProperty.build(
85+
name=name,
86+
required=required,
87+
default=None,
88+
python_name=python_name,
89+
description=data.description,
90+
example=data.example,
91+
)
8292
return StringProperty.build(
8393
name=name,
8494
default=data.default,

openapi_python_client/parser/properties/property.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .none import NoneProperty
1919
from .string import StringProperty
2020
from .union import UnionProperty
21+
from .uuid import UuidProperty
2122

2223
Property: TypeAlias = Union[
2324
AnyProperty,
@@ -34,4 +35,5 @@
3435
NoneProperty,
3536
StringProperty,
3637
UnionProperty,
38+
UuidProperty,
3739
]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, ClassVar
4+
from uuid import UUID
5+
6+
from attr import define
7+
8+
from ... import schema as oai
9+
from ...utils import PythonIdentifier
10+
from ..errors import PropertyError
11+
from .protocol import PropertyProtocol, Value
12+
13+
14+
@define
15+
class UuidProperty(PropertyProtocol):
16+
"""A property of type uuid.UUID"""
17+
18+
name: str
19+
required: bool
20+
default: Value | None
21+
python_name: PythonIdentifier
22+
description: str | None
23+
example: str | None
24+
25+
_type_string: ClassVar[str] = "uuid.UUID"
26+
_json_type_string: ClassVar[str] = "str"
27+
_allowed_locations: ClassVar[set[oai.ParameterLocation]] = {
28+
oai.ParameterLocation.QUERY,
29+
oai.ParameterLocation.PATH,
30+
oai.ParameterLocation.COOKIE,
31+
oai.ParameterLocation.HEADER,
32+
}
33+
template: ClassVar[str] = "uuid_property.py.jinja"
34+
35+
@classmethod
36+
def build(
37+
cls,
38+
name: str,
39+
required: bool,
40+
default: Any,
41+
python_name: PythonIdentifier,
42+
description: str | None,
43+
example: str | None,
44+
) -> UuidProperty | PropertyError:
45+
checked_default = cls.convert_value(default)
46+
if isinstance(checked_default, PropertyError):
47+
return checked_default
48+
49+
return cls(
50+
name=name,
51+
required=required,
52+
default=checked_default,
53+
python_name=python_name,
54+
description=description,
55+
example=example,
56+
)
57+
58+
@classmethod
59+
def convert_value(cls, value: Any) -> Value | None | PropertyError:
60+
if value is None or isinstance(value, Value):
61+
return value
62+
if isinstance(value, str):
63+
try:
64+
UUID(value)
65+
except ValueError:
66+
return PropertyError(f"Invalid UUID value: {value}")
67+
return Value(value)
68+
if isinstance(value, UUID):
69+
return Value(str(value))
70+
return PropertyError(f"Invalid UUID value: {value}")
71+
72+
def get_imports(self, *, prefix: str) -> set[str]:
73+
"""
74+
Get a set of import strings that should be included when this property is used somewhere
75+
76+
Args:
77+
prefix: A prefix to put before any relative (local) module names. This should be the number of . to get
78+
back to the root of the generated client.
79+
"""
80+
imports = super().get_imports(prefix=prefix)
81+
imports.update({"import uuid"})
82+
return imports
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{% macro construct_function(property, source) %}
2+
uuid.UUID({{ source }})
3+
{% endmacro %}
4+
5+
{% from "property_templates/property_macros.py.jinja" import construct_template %}
6+
7+
{% macro construct(property, source) %}
8+
{{ construct_template(construct_function, property, source) }}
9+
{% endmacro %}
10+
11+
{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %}
12+
13+
{% macro transform(property, source, destination, declare_type=True) %}
14+
{% set transformed = "str(" + source + ")" %}
15+
{% if property.required %}
16+
{{ destination }} = {{ transformed }}
17+
{%- else %}
18+
{% if declare_type %}
19+
{% set type_annotation = property.get_type_string(json=True) %}
20+
{{ destination }}: {{ type_annotation }} = UNSET
21+
{% else %}
22+
{{ destination }} = UNSET
23+
{% endif %}
24+
if not isinstance({{ source }}, Unset):
25+
{{ destination }} = {{ transformed }}
26+
{%- endif %}
27+
{% endmacro %}
28+
29+
{% macro transform_multipart(property, source, destination) %}
30+
{% if property.required %}
31+
{{ destination }} = str({{ source }})
32+
{%- else %}
33+
{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %}
34+
{{ destination }}: {{ type_annotation }} = UNSET
35+
if not isinstance({{ source }}, Unset):
36+
{{ destination }} = str({{ source }})
37+
{%- endif %}
38+
{% endmacro %}

0 commit comments

Comments
 (0)